会使用自定义注解 ≈ 好的程序员?教你结合 AOP 切面打印请求日志

一、前言

今天就带着大伙梳理一遍注解也就是 @interface 正确的打开方式,除此之外,结合 AOP 切面统一打印出入参日志,对于每个访问注解绑定的接口方法的请求都一目了然,不仅方便接口的调试,还能给你一个优雅、整齐且大方的控制台日志记录。

二、效果演示

2.1 访问接口

2.2 控制台日志输出

三、如何设计一个注解

3.1 概念

知其然,要知其所以然,所以我们先来康康官方对注解的描述是什么:

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

翻译过来的大意是:

注释是一种元数据的形式,可以添加到Java源代码中。类,方法,变量,参数和包可以注释。注释对他们注释的代码的操作没有直接影响。

综上来说,注解其实相当于 Java 的一种特殊的数据类型,也可以把它当做一个可以自定义的标记去理解,和类、接口、枚举类似,可以使用在很多不同的地方并且对原有的操作代码没有任何影响,仅做中间收集和处理。

3.2 小试牛刀


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {

    public String name();

    int sort() default Integer.MAX_VALUE;

    ... ...
}

说明(从上往下):

  • 使用该注解在程序运行时被 JVM 保留,并且被编译器记录到 class 文件中,所以能够通过 Java 反射机制读取到注解中的属性等
  • 该注解仅能使用在字段上,不能用在类、方法、变量上
  • 该注解有两个属性,一个是 name 另一个是 sort 属性,属性?你可能就会问了后边不是带一对圆括号嘛,不应该是方法吗?看似接口中定义的抽象方法,实则看没看到 default 关键字,官方管定义在注解内的是 注解类型元素 ,不过我习惯管它们叫属性,因为在使用注解时,总是以键值对的形式传参
  • 访问修饰符必须为 public,不写默认为 public
  • 圆括号不是定义方法参数的地方,也不能在括号中定义任何参数,这仅仅是一个特殊的写法罢了
  • default 表示未设置该属性时的默认值,值需和类型保持一致
  • 如果没有 default 默认值,表示该类型元素必须在后续赋值

3.3 注解注解的注解类

此外,在 JDK 中提供了 4 个标准的用来对注解类型进行注解的注解类(元注解),分别是:

  • @Target 是专门用来限定某个自定义注解能够被应用在哪些 Java 元素上

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    /** 类,接口(包括注释类型)或枚举声明 */
    TYPE,

    /** Field declaration (includes enum constants) */
    /** 字段声明(包括枚举常量) */
    FIELD,

    /** Method declaration */
    /** 方法声明 */
    METHOD,

    /** Formal parameter declaration */
    /** 形参声明 */
    PARAMETER,

    /** Constructor declaration */
    /** 构造器声明 */
    CONSTRUCTOR,

    /** Local variable declaration */
    /** 本地变量声明 */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    /** 注解类型声明 */
    ANNOTATION_TYPE,

    /** Package declaration */
    /** 包声明 */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    /** 类型参数声明 */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    /** 使用类型 */
    TYPE_USE
}
  • @Retention 该注解有 “保留”、“保持” 之义,用来定义注解的留存策略,可指定的留存策略只有 3 个:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     *
     * 编译器丢弃注解,即被编译器忽略
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     *
     * 注释将被编译器记录在 class 文件中,但在运行时不需要被虚拟机保留。这是一个默认的行为。
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * 注释将由编译器记录在类文件中,并在运行时由虚拟机保留,因此可以通过反射读取。
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

一般使用无特殊需要,使用 RetentionPolicy.RUNTIME 就够了。

  • @Documented 是被用来指定自定义注解是否能随着被定义的 Java 文件生成到 JavaDoc 文档当中
  • @Inherited 是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解

注意:@Inherited 注解只对那些 @Target 被定义为 ElementType.TYPE 的自定义注解起作用。

3.4 使用流程

image.gif

四、代码实现

4.1 第一步

编写设计注解:


/**
 * @description: TODO
 * @author: HUALEI
 * @date: 2021-11-19
 * @time: 15:50
 */
@Retention(RetentionPolicy.RUNTIME)
/* 注解用在方法上 */
@Target(ElementType.METHOD)
public @interface MyAnnotation {

    /**
     * 接口方法描述
     */
    public String description() default "默认描述";
}

这步没什么好讲的,上面的概念理解掌握了,轻轻松松写出这个注解应该是没有什么问题!

4.2 第二步

使用切面注解进行标记,因为是对请求相关的日志打印,所以我们随便写一个控制层接口方法进行测试:


/**
 * @description: TODO
 * @author: HUALEI
 * @date: 2021-11-24
 * @time: 13:43
 */
@RestController
@RequestMapping(value = "/test")
public class TestController {

    private final static Logger log = LoggerFactory.getLogger(TestController.class);

    @GetMapping("/hello/{say}")
    @MyAnnotation(description = "测试接口")
    public String sayHello(@PathVariable("say") String content) {
        log.info("Client is saying:{}", content);
        return content;
    }
}

4.3 第三步

最后一步也是最关键的一步,在运行时解析注解执行切面操作,所以对应地写一个切面类:

image.gif

新建切面类后,考虑到日志的打印,这段代码必不可少:


/**
 * @description: TODO
 * @author: HUALEI
 * @date: 2021-11-19
 * @time: 15:56
 */
@Aspect
@Component
public class MyAnnotationAspect {

    private static final Logger logger = LoggerFactory.getLogger(MyAnnotationAspect.class);

    ......
    ......
}

@Aspect 和 @Component 注解必不可少,@Component 大伙应该在熟悉不过了,将该类注入到 Spring 容器中;而另一个 @Aspect 注解的作用是把当前类标识成一个切面供容器去读取。

注意: 打印日志推荐使用的包是 slf4j.Logger 。


/**
 * 配置织入点
 *
 * 切到所有被 @MyAnnotation 注解修饰的方法
 */
@Pointcut("@annotation(com.xxx.xxx.annotation.MyAnnotation)")
// @annotation(annotationType) 匹配指定注解为切入点的方法,annotationType 为注解的全路径
public void myAnnotationPointCut() {
}

配置织入点,切到所有被 @MyAnnotation 注解修饰的方法,不需要再方法体内编写实际的代码!


/**
 * 环绕增强,可自定义目标方法执行的时机
 * 实现记录所有被 @MyAnnotation 注解修饰接口请求功能
 *
 * @param pjp 连接点对象
 * @return 目标方法的返回值
 * @throws Throwable 异常
 */
@Around("myAnnotationPointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    // 请求开始时间戳
    // long begin = System.currentTimeMillis();

    TimeInterval timer = DateUtil.timer();

    // 通过请求上下文(执行目标方法之前)
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

    HttpServletRequest request = attributes.getRequest();

    // 获取连接点的方法签名对象
    Signature signature = pjp.getSignature();
    MethodSignature methodSignature = (MethodSignature) signature;

    // 获取接口方法
    Method method = methodSignature.getMethod();
    // 通过接口方法获取该方法上的 @MyAnnotation 注解对象
    MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);

    // 通过注解获取接口方法描述信息
    String description = myAnnotation.description();

    // 请求开始(前置通知)
    logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 请求开始 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
    // 请求链接
    logger.info("请求链接:{}", request.getRequestURL().toString());
    // 接口方法描述信息
    logger.info("接口描述:{}", description);
    // 请求类型
    logger.info("请求类型:{}", request.getMethod());
    // 请求方法
    logger.info("请求方法:{}.{}", signature.getDeclaringTypeName(), signature.getName());
    // 请求远程地址
    logger.info("请求远程地址:{}", request.getRemoteAddr());
    // 请求入参
    logger.info("请求入参:{}", JSONUtil.toJsonStr(pjp.getArgs()));

    // 请求结束时间戳
    // long end = System.currentTimeMillis();

    // 请求耗时
    logger.info("请求耗时:{}", timer.intervalPretty());

    // 请求返回结果(执行目标方法之后)
    Object processedResult = pjp.proceed();
    // 请求返回
    logger.info("请求返回:{}", JSONUtil.toJsonStr(processedResult));

    // 请求结束(后置通知)
    logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 请求结束 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + System.lineSeparator());

    return processedResult;
}

pjp 连接点对象,JoinPoint 的子接口,可以获取当前切入的方法的参数、代理类等信息,因此可以记录一些信息、验证一些信息等,它有两个重要的方法:

  • Object proceed() throws Throwable 执行目标方法
  • Object proceed(Object[] var1) throws Throwable 传入的新的参数去执行目标方法

整个代码都有注解,这里就不赘述代码逻辑了!

4.4 扩展

除了上面用到的 @PointCut 和 @Around 注解,还有另外 4 个使用 AOP 常用的注解:

  • @Before :前置增强,在切点之前织入相关代码
  • @After :final 增强,不管是抛出异常或者正常退出都会执行
  • @AfterReturning :后置增强,方法正常退出时执行
  • @AfterThrowing :异常抛出增强,切点方法抛出异常时执行

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

有兴趣的同学,可以环绕增强中的代码拆分到前置和后置增强中,以便更好地理解这四个常用注解使用场景! (๑•̀ㅂ•́)و✧

作者:HUALEI
链接:
https://juejin.cn/post/7034406941925474318

你可能感兴趣的:(会使用自定义注解 ≈ 好的程序员?教你结合 AOP 切面打印请求日志)