【SpringBoot】SpringBoot 中的 切面 AOP + 自定义注解

1、 @AspectJ

1.1、@AspectJ 切面类

Spring 提供了四种aop切面的支持:

  • 基于代理的经典的 Spring Aop
  • 纯POJO切面
  • @AspectJ 注解驱动切面(底层也是 Spring 的动态代理)
  • 注入式 Aspectj 切面

而本此使用的就是 @AspectJ 注解驱动切面的方式。

在配置 AOP 切面之前,我们需要了解下 aspectj 相关注解的作用:

  • @Aspect:声明该类为一个切面类;
  • @Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为切某个注解;也可以切某个 package 下的方法;

1.2、@Pointcut 创建切入点。

切入点方法不用写代码,返回类型为 void。

execution:用于匹配表达式。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

表达式含义:

  • modifier-pattern?:修饰符
  • ret-type-pattern:返回值。可以为*,表示任何返回值,全路径的类名等
  • declaring-type-pattern?:类路径
  • name-pattern:方法名。可以指定方法名 或者 代表所有,set 代表以set开头的所有方法
  • param-pattern:参数。可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“*”来表示匹配任意类型的参数。如:(String) 表示匹配一个 String 参数的方法;(*, String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(..)表示零个或多个任意参数
  • throws-pattern?:异常类型。其中后面跟着 “?” 的是可选项

举例:

  • execution(* *(..)) 表示匹配所有方法
  • execution(public * com. example.controller.*(..)) 表示匹配 com. example.controller 中所有的 public 方法
  • execution(* com. example.controller..*.*(..)) 表示匹配 com. example.controller 包及其子包下的所有方法

1.3、通知

通知有 5 种:

  • @Before:前置通知。目标方法调用前被调用
  • @After:最终通知。目标方法执行完之后执行
  • @AfterReturning:后置返回通知。如果参数中的第一个参数为 JoinPoint,则第二个参数为返回值的信息;如果参数中的第一个参数不为 JoinPoint,则第一个参数为 returning 中对应的参数;returning 只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行
  • @AfterThrowing:后置异常通知。
  • @Around:环绕通知。环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint 类型

执行顺序:@Around => @Before => 接口中的逻辑代码 => @After => @AfterReturning

除 @Around 外,每个方法里都可以加或者不加参数 JoinPoint。JoinPoint 包含了类名、被切面的方法名、参数等属性。

@Around 参数必须为 ProceedingJoinPoint。

1.4、Spring AOP 和 AspectJ AOP 有什么区别?

  • Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。

  • Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

  • Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

  • AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单。

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。

2、在 SpringBoot 中使用 Aop 功能

引入 POM 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

1、编写测试 Controller:

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/testHello")
    public String testHello(String params){
        return "Hello World";
    }

}

2、编写切面类(使用注解 @Aspect):

@Slf4j
@Aspect
@Component
public class AopAspect {

    @Before("execution(public * com.tinady.controller.AopController.*(..))")
    public void doBefore(JoinPoint point){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        log.info("调用前连接点方法为:" + methodName + ",参数为:" + JSON.toJSONString(args));
    }

    @AfterReturning(value = "execution(public * com.tinady.controller.AopController.*(..))", returning = "returnValue")
    public void doAfterReturning(JoinPoint point, Object returnValue){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        log.info("调用前连接点方法为:" + methodName + ",参数为:" + JSON.toJSONString(args) + ",返回值为:" + JSON.toJSONString(returnValue));
    }
    
}

说明:

  1. @Before:定义了前置通知方法。打印出入参
  2. @AfterReturning:定义了后置返回通知方法。打印出入参、返参

调用上述接口后,查看控制台:
在这里插入图片描述
将上述的两个通知方法抽出一个切入点(两个通知方法的 execution 重复,提取出来):

@Slf4j
@Aspect
@Component
public class AopAspect {

    // 切入点
    @Pointcut("execution(public * com.tinady.controller.AopController.*(..))")
    public void doPointCut() {

    }

    @Before("doPointCut()")
    public void doBefore(JoinPoint point){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        log.info("调用前连接点方法为:" + methodName + ",参数为:" + JSON.toJSONString(args));
    }

    @AfterReturning(value = "doPointCut()", returning = "returnValue")
    public void doAfterReturning(JoinPoint point, Object returnValue){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        log.info("调用前连接点方法为:" + methodName + ",参数为:" + JSON.toJSONString(args) + ",返回值为:" + JSON.toJSONString(returnValue));
    }

}

依旧能达到之前的效果。

3、AOP 中使用自定义注解

添加一个自定义注解 @MyAop

package com.tinady.annotation;

public @interface MyAop {
    String value() default "自定义注解拦截";
}

将此注解添加到需要拦截的方法上:

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/MyAop")
    @MyAop
    public String MyAop(String params){
        return "MyAop";
    }

}

定义一个切面类:

@Slf4j
@Aspect
@Component
public class AopAspect {

    @Before("@annotation(com.tinady.annotation.MyAop)")
    public void doBefore(JoinPoint point){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        log.info("调用前连接点方法为:" + methodName + ",参数为:" + JSON.toJSONString(args));
    }
    
}

4、Aop + 自定义注解实现日志记录

定义一个 Controller:

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/tetLog")
    @MyOperationLog(methodName = "testLog", currentUser = "admin", operate = "查询")
    public String testLog() {
        return "Log";
    }

}

在这个 Controller 中,给需要拦截的方法加上注解:@MyOperationLog(自定义的)。

定义一个自定义注解 @MyOperationLog

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyOperationLog {

    // 方法名称
    String methodName() default "";

    // 当前操作人
    String currentUser() default "";

    // 操作
    String operate() default "";

}

定义一个切面类 @MyLogAspect

@Slf4j
@Aspect
@Component
public class MyLogAspect {
	
	@Autowired
    private AspectUtil aspectUtil;

    @Autowired
    private LogService logService;
	
	// 定义一个切入点
    @Pointcut("@annotation(com.tinady.annotation.MyOperationLog)")
    public void doPointcut() {
        log.info("进入切入点~~~");
    }
	
	// 定义一个通知
    @Before("doPointcut()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("进入前置通知");
        doHandlerLog(joinPoint);
    }
}

MyLogAspect#doHandlerLog():做日志记录处理

private void doHandlerLog(JoinPoint joinPoint) {
    // 获取自定义注解@MyOperationLog
    MyOperationLog myOperationLog = aspectUtil.getMyOperationLogByJoinPoint(joinPoint);
    if (null == myOperationLog) {
        return;
    }
    // 获取签名
    String signature = joinPoint.getSignature().toString();
    // 获取方法名
    String methodName = signature.substring(signature.lastIndexOf(".") + 1, signature.indexOf("("));
    // 获取方法的execution
    String longTemp = joinPoint.getStaticPart().toLongString();
    String classType = joinPoint.getTarget().getClass().getName();
    try {
        Class<?> clazz = Class.forName(classType);
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(MyOperationLog.class) && method.getName().equals(methodName)) {
            	// 解析
                MyOperationLogVo myOperationLogVos = parseAnnotation(method);
                // 日志添加
                logService.addLog(myOperationLogVos);
            }
        }
    } catch (ClassNotFoundException ex) {
        ex.printStackTrace();
    }
}

MyLogAspect#parseAnnotation():解析方法上注解中的值

// 解析方法上注解中的值
public MyOperationLogVo parseAnnotation(Method method) {
    MyOperationLog myOperationLog = method.getAnnotation(MyOperationLog.class);
    if (null == myOperationLog) {
        return null;
    }
    MyOperationLogVo myOperationLogVo = new MyOperationLogVo();
    myOperationLogVo.setMethodName(myOperationLog.methodName());
    myOperationLogVo.setCurrentUser(myOperationLog.currentUser());
    myOperationLogVo.setOperate(myOperationLog.operate());
    return myOperationLogVo;
}

AspectUtil

@Component
public class AspectUtil {

    /**
    *
    * 功能描述:通过JoinPoint获取注解MyOperationLog
     *
     * 访问目标方法参数,有三种方法(实际有四种):
     * 1.joinpoint.getargs():获取带参方法的参数
     * 2.joinpoint.getTarget():.获取他们的目标对象信息
     * 3.joinpoint.getSignature():(signature是信号,标识的意思):获取被增强的方法相关信息
    *
    */
    public MyOperationLog getMyOperationLogByJoinPoint(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (null == method) {
            return null;
        }
        return method.getAnnotation(MyOperationLog.class);
    }

}

LogServiceImpl:记录日志

@Slf4j
@Service
public class LogServiceImpl implements LogService {

    @Override
    public void addLog(MyOperationLogVo myOperationLogVo) {
        log.info("{}", JSON.toJSONString(myOperationLogVo));
    }

}

5、Aop + 自定义注解实现统一打印出入参日志

先看下切面日志输出效果咋样:
在这里插入图片描述
从上图中可以看到,每个对于每个请求,开始与结束一目了然,并且打印了以下参数:

  • URL: 请求接口地址;
  • VALUE: 接口的中文说明信息;
  • HTTP Method: 请求的方法,是 POST、 GET、 还是 DELETE 等;
  • Class Method: 被请求的方法路径 : 包名 + 方法名;
  • IP: 请求方的 IP 地址;
  • Request Args: 请求入参,以 JSON 格式输出;
  • Response Args: 响应出参,以 JSON 格式输出;
  • Time-Consuming: 请求耗时,以此估算每个接口的性能指数;

定义一个自定义注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
    String value() default "";
}

定义一个切面类,声明一个切入点:

@Slf4j
@Aspect
@Component
public class MyAspect {

    private static final String LINE_SEPARATOR = System.lineSeparator();

    @Pointcut("@annotation(com.tinady.annotation.MyLog)")
    public void doPointCut() {}

    @After("doPointCut()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("========================== END ==========================" + LINE_SEPARATOR);
    }

}

定义一个环绕通知 MyAspect#doAround():用于何时执行切入点

@Around("doPointCut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    // 执行切入点
    Object result = proceedingJoinPoint.proceed();
    // 出参
    log.info("方法返参为:{}", JSON.toJSONString(result));
    // 执行耗时
    log.info("Time-Consuming:{} ms", System.currentTimeMillis() - startTime);
    return result;
}

执行切点后,会去依次调用 @Before -> 接口逻辑代码 -> @After -> @AfterReturning

定义一个前置通知 MyAspect#doBefore()

@Before("doPointCut()")
public void doBefore(JoinPoint joinPoint) throws Exception{
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = servletRequestAttributes.getRequest();
    // 获取注解信息
    String value = getAspectLogValue(joinPoint);

    // 请求相关信息
    log.info("========================== START ==========================");
    log.info("URL               : {}", request.getRequestURL().toString());
    log.info("VALUE             : {}", value);
    log.info("HTTP Method       : {}", request.getMethod());
    log.info("Class Method      : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
    log.info("IP                : {}", request.getRemoteAddr());
    log.info("Request Args      : {}", JSON.toJSONString(joinPoint.getArgs()));
}

MyAspect#getAspectLogValue():获取注解信息

public String getAspectLogValue(JoinPoint joinPoint) throws ClassNotFoundException {
    String targetName = joinPoint.getTarget().getClass().getName();
    String methodName = joinPoint.getSignature().getName();
    Object[] arguments = joinPoint.getArgs();
    Class targetClass = Class.forName(targetName);
    Method[] methods = targetClass.getMethods();
    StringBuilder value = new StringBuilder();
    for (Method method : methods) {
        if (method.getName().equals(methodName)) {
            Class[] clazzs = method.getParameterTypes();
            if (clazzs.length == arguments.length) {
                value.append(method.getAnnotation(MyLog.class).value());
                break;
            }
        }
    }
    return value.toString();
}

定义一个 Controller:

因为我们的切点是自定义注解 @MyLog,所以我们仅仅需要在 Controller 控制器的每个接口方法添加 @MyLog 注解即可,如果我们不想某个接口打印出入参日志,不加注解就可以了:

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/testMyLog")
    @MyLog("请求了testMyLog方法")
    public String testMyLog() {
        return "My Log";
    }

}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值