0
点赞
收藏
分享

微信扫一扫

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

飞进科技 2022-01-22 阅读 40

一、前言

二、效果演示

2.1 访问接口

2.2 控制台日志输出

三、如何设计一个注解

3.1 概念

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

翻译过来的大意是:

综上来说,注解其实相当于 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 使用流程

四、代码实现

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 第三步

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

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


/**
 * @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

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

举报

相关推荐

0 条评论