使用自定义注解和切面AOP实现Java程序增强

1. 注解介绍

1.1 注解的本质

Oracle 官方对注解的定义为:

Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

注解是元数据的一种形式,它提供有关程序的数据,该数据不属于程序本身。 注解对其注释的代码操作没有直接影响。

而在JDK的 Annotation 接口中有一行注释如此写到:

/**
* The common interface extended by all annotation types. 
* ...
*/
public interface Annotation {...}

这说明其他注解都扩展自 Annotation 这个接口,也就是说注解的本质就是一个接口。
以 Spring Boot 中的一个注解为例:

        @Target({ElementType.TYPE})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @Indexed
        public @interface Component {
            String value() default "";
        }

它实际上相当于:

        public interface Component extends Annotation{...}

@interface 可以看成是一个语法糖。

1.2 注解的要素

依然来看 @Component 这个例子:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";
}

在注解定义上有几个注解@Target, @Retention, @Documented,被称为 元注解

所谓元注解就是说明注解的注解

Java 中的元注解共有以下几个:

1.2.1 @Target

@Target顾名思义,这个注解标识了被修饰注解的作用对象。我们看看它的源码:

        @Documented
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.ANNOTATION_TYPE)
        public @interface Target {
            /**
             * Returns an array of the kinds of elements an annotation type
             * can be applied to.
             * @return an array of the kinds of elements an annotation type
             * can be applied to
             */
            ElementType[] value();
        }

可以看到,这个注解的 value 值是一个数组,这也就意味着注解的作用对象可以有多个。 其取值范围都在ElementType这个枚举之中:

        public enum ElementType {
            /** 类、接口、枚举定义 */
            TYPE,
            /** 字段,包括枚举值 */
            FIELD,
            /** 方法 */
            METHOD,
            /** 参数 */
            PARAMETER,
            /** 构造方法 */
            CONSTRUCTOR,
            /** 局部变量 */
            LOCAL_VARIABLE,
            /** 元注解 */
            ANNOTATION_TYPE,
            /** 包定义 */
            PACKAGE...
        }

不同的值代表被注解可修饰的范围,例如TYPE只能修饰类、接口和枚举定义。这其中有个很特殊的值叫做 ANNOTATION_TYPE, 是专门表示元注解的。

在回过头来看 @Component 这个例子, Target 取值为 TYPE。熟悉 Spring Boot 的同学也一定知道,@Component 确实是不能放到方法或者属性前面的。

1.2.2@Retention

@Retention 注解指定了被修饰的注解的生命周期。定义如下:

        @Documented
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.ANNOTATION_TYPE)
        public @interface Retention {
            /**
             * Returns the retention policy.
             * @return the retention policy
             */
            RetentionPolicy value();
        }

可以看到这个注解带一个 RetentionPolicy 的枚举值:

        public enum RetentionPolicy {
            SOURCE,
            CLASS,
            RUNTIME
        }
  • SOURCE 表示注解编译时可见,编译完后就被丢弃。这种注解一般用于在编译器做一些事情;
  • CLASS 表示在编译完后写入 class 文件,但在类加载后被丢弃。这种注解一般用于在类加载阶段做一些事情;
  • RUNTIME 则表示注解会一直起作用。

1.2.3 @Documented

这个注解比较简单,表示是否添加到 java doc 中。

1.2.4 @Inherited

这个也比较简单,表示注解是否被继承。这个注解不是很常用。

注意:元注解只在定义注解时被使用!

1.3 注解的构成

从上面的元注解可以了解到,一个注解可以关联多个 ElementType,但只能有一个 RetentionPolicy:

注解的构成

Java 中有三个常用的内置注解,其实相信大家都用过或者见过。不过在了解了注解的真实面貌以后,不妨重新认识一下吧!

1.4 Java 内置注解

1.4.1 @Override

@Override它的定义为:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

可见这个注解没有任何取值,只能修饰方法,而且 RetentionPolicy 为 SOURCE,说明这是一个仅在编译阶段起作用的注解。

它的真实作用想必大家一定知道,就是在编译阶段,如果一个类的方法被 @Override 修饰,编译器会在其父类中查找是否有同签名函数,如果没有则编译报错。可见这确实是一个除了在编译阶段就没什么用的注解。

1.4.2 @Deprecated

@Deprecated它的定义为:

        @Documented
        @Retention(RetentionPolicy.RUNTIME)
        @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
        public @interface Deprecated {
        }

这个注解也没有任何取值,能修饰所有的类型,永久存在。这个注解的作用是,告诉使用者被修饰的代码不推荐使用了,可能会在下一个软件版本中移除。这个注解仅仅起到一个通知机制,如果代码调用了被 @Deprecated 修饰的代码,编译器在编译时输出一个编译告警。

1.4.3 @SuppressWarnings

@SuppressWarnings它的定义为:

        @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
        @Retention(RetentionPolicy.SOURCE)
        public @interface SuppressWarnings {
            /**
             * The set of warnings that are to be suppressed by the compiler in the
             * annotated element.  Duplicate names are permitted.  The second and
             * successive occurrences of a name are ignored.  The presence of
             * unrecognized warning names is <i>not</i> an error: Compilers must
             * ignore any warning names they do not recognize.  They are, however,
             * free to emit a warning if an annotation contains an unrecognized
             * warning name.
             *
             * <p> The string {@code "unchecked"} is used to suppress
             * unchecked warnings. Compiler vendors should document the
             * additional warning names they support in conjunction with this
             * annotation type. They are encouraged to cooperate to ensure
             * that the same names work across multiple compilers.
             * @return the set of warnings to be suppressed
             */
            String[] value();
        }     

这个注解有一个字符串数组的值,需要我们使用注解的时候传递。可以在类型、属性、方法、参数、构造函数和局部变量前使用,声明周期是编译期。

这个注解的主要作用是压制编译告警的。

1.5 注解的工作原理

这一块我就贴出来 不做详细的介绍

  1. 通过键值对的形式为注解属性赋值
  2. 编译器检查注解的使用范围, 将注解信息写入元素属性表
  3. 运行时 JVM 将 RUNTIME 的所有注解属性取出来并最终存入 map 里
  4. JVM 创建 AnnotationInvocationHandler 实例并传入到前面的 map 里
  5. JVM 使用 JDK 动态代理为注解生成代理类, 并初始化处理器
  6. 调用 invoke 方法, 通过传入方法名返回注解对应的属性值

2.AOP 介绍(AspectJ 暂不讨论)

2.1 Spring AOP 基本概念

  1. 是一种动态编译期增强性 AOP 的实现
  2. 与 IOC 进行整合,不是全面的切面框架
  3. 与动态代理相辅相成
  4. 有两种实现:基于 jdk 动态代理、cglib

2.2 Spring AOP 与 AspectJ 区别

  1. Spring 的 AOP 是基于动态代理的,动态增强目标对象,而 AspectJ 是静态编译时增强,需要使用自己的编译器来编译,还需要织入器
  2. 使用 AspectJ 编写的 java 代码无法直接使用 javac 编译,必须使用 AspectJ 增强的 ajc 增强编译器才可以通过编译,写法不符合原生 Java 的语法;而 Spring AOP 是符合 Java 语法的,也不需要指定编译器去编译,一切都由 Spring 处理。

2.3 使用步骤

  1. 定义业务组件
  2. 定义切点(重点)
  3. 定义增强处理方法 (切面方法)

这边用下面例子的 AOP 类来进行说明 (基于 Spring AOP 的)

    /**
     * @Author Song
     * @Date 2020/5/26 9:50
     * @Version 1.0
     */
    @Slf4j
    @Aspect
    @Component
    public class EagleEyeAspect {
    <span class="hljs-meta">@Pointcut("@annotation(com.ctgu.song.plantfactory.v2.annotation.EagleEye)")</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">eagleEye</span><span class="hljs-params">()</span> {

    }


    <span class="hljs-meta">@Around("eagleEye()")</span>
    <span class="hljs-keyword">public</span> Object <span class="hljs-title function_">around</span><span class="hljs-params">(ProceedingJoinPoint pjp)</span> <span class="hljs-keyword">throws</span> Throwable {
        <span class="hljs-type">long</span> <span class="hljs-variable">begin</span> <span class="hljs-operator">=</span> System.currentTimeMillis();
        <span class="hljs-type">ServletRequestAttributes</span> <span class="hljs-variable">attributes</span> <span class="hljs-operator">=</span> (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        <span class="hljs-type">HttpServletRequest</span> <span class="hljs-variable">request</span> <span class="hljs-operator">=</span> attributes.getRequest();
        <span class="hljs-type">Signature</span> <span class="hljs-variable">signature</span> <span class="hljs-operator">=</span> pjp.getSignature();
        <span class="hljs-type">MethodSignature</span> <span class="hljs-variable">methodSignature</span> <span class="hljs-operator">=</span> (MethodSignature) signature;
        <span class="hljs-type">Method</span> <span class="hljs-variable">method</span> <span class="hljs-operator">=</span> methodSignature.getMethod();
        <span class="hljs-type">EagleEye</span> <span class="hljs-variable">eagleEye</span> <span class="hljs-operator">=</span> method.getAnnotation(EagleEye.class);
        <span class="hljs-type">String</span> <span class="hljs-variable">desc</span> <span class="hljs-operator">=</span> eagleEye.desc();
        log.info(<span class="hljs-string">"============请求开始=========="</span>);
        log.info(<span class="hljs-string">"请求链接:{}"</span>, request.getRequestURI().toString());
        log.info(<span class="hljs-string">"接口描述:{}"</span>, desc);
        log.info(<span class="hljs-string">"请求类型:{}"</span>, request.getMethod());
        log.info(<span class="hljs-string">"请求方法:{}.{}"</span>, signature.getDeclaringTypeName(), signature.getName());
        log.info(<span class="hljs-string">"请求IP:{}"</span>, request.getRemoteAddr());
        log.info(<span class="hljs-string">"请求入参:{}"</span>, JSON.toJSONString(pjp.getArgs()));
        <span class="hljs-type">Object</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> pjp.proceed();
        <span class="hljs-type">long</span> <span class="hljs-variable">end</span> <span class="hljs-operator">=</span> System.currentTimeMillis();
        log.info(<span class="hljs-string">"请求耗时:{}ms"</span>, end - begin);
        log.info(<span class="hljs-string">"请求返回:{}"</span>, JSON.toJSONString(result));
        log.info(<span class="hljs-string">"=============请求结束==========="</span>);
        <span class="hljs-keyword">return</span> result;
    }
}

这边先不看代码的具体内容,先简单介绍一下用到 AOP 中常用的注解

  • @Aspect : 指定切面类;
  • @Pointcut:公共切入点表达式
  • 通知方法
    • 前置通知(@Before) 目标方法执行之前,执行注解的内容
    • 后置通知(@After)目标方法执行之后,执行注解的内容
    • 返回通知 (@AfterReturning)目标方法返回后,执行注解的内容
    • 异常通知 (@AfterThrowing)目标方法抛出异常后,执行注解的内容
    • 环绕通知 (@Around)目标方法执行前后,分别执行一些代码

注意 定义好切片类后要将其加入 Spring 容器内才能使用哦 (可以使用 @Component 注解)

3. 具体实现(一个例子)

1. 首先定义一个注解,代码如下

        /**
         * @Author Song
         * @Date 2020/5/26 9:44
         * @Version 1.0
         */
    <span class="hljs-meta">@Retention(RetentionPolicy.RUNTIME)</span>
    <span class="hljs-meta">@Target(ElementType.METHOD)</span>
    <span class="hljs-meta">@Documented</span>
    <span class="hljs-keyword">public</span> <span class="hljs-meta">@interface</span> EagleEye {
    
        <span class="hljs-comment">/**
         * <span class="hljs-doctag">@Retention</span>(RetentionPolicy.RUNTIME)
         * 定义了注解的生命周期为运行时
         * &lt;p&gt;
         * <span class="hljs-doctag">@Target</span>(ElementType.METHOD)
         * 定义了注解的作用域为方法
         * &lt;p&gt;
         * Documented
         * 标识该注解可以被JavaDoc记录
         * &lt;p&gt;
         * 定义注解名称为EagleEye(鹰眼,哈哈~~)
         * &lt;p&gt;
         * 定义一个元素desc,用来描述被修饰的方法
         * &lt;p&gt;
         * 接口描述
         *
         * <span class="hljs-doctag">@return</span>
         */</span>
        String <span class="hljs-title function_">desc</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-string">""</span>;
    }

2. 定义切片内并写好自己想要增强的方法

直接贴代码了 ~~

        /**
         * @Author Song
         * @Date 2020/5/26 9:50
         * @Version 1.0
         */
        @Slf4j
        @Aspect
        @Component
        public class EagleEyeAspect {
        <span class="hljs-meta">@Pointcut("@annotation(com.ctgu.song.plantfactory.v2.annotation.EagleEye)")</span>
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">eagleEye</span><span class="hljs-params">()</span> {
    
        }
    
    
        <span class="hljs-meta">@Around("eagleEye()")</span>
        <span class="hljs-keyword">public</span> Object <span class="hljs-title function_">around</span><span class="hljs-params">(ProceedingJoinPoint pjp)</span> <span class="hljs-keyword">throws</span> Throwable {
            <span class="hljs-type">long</span> <span class="hljs-variable">begin</span> <span class="hljs-operator">=</span> System.currentTimeMillis();
            <span class="hljs-type">ServletRequestAttributes</span> <span class="hljs-variable">attributes</span> <span class="hljs-operator">=</span> (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            <span class="hljs-type">HttpServletRequest</span> <span class="hljs-variable">request</span> <span class="hljs-operator">=</span> attributes.getRequest();
            <span class="hljs-type">Signature</span> <span class="hljs-variable">signature</span> <span class="hljs-operator">=</span> pjp.getSignature();
            <span class="hljs-type">MethodSignature</span> <span class="hljs-variable">methodSignature</span> <span class="hljs-operator">=</span> (MethodSignature) signature;
            <span class="hljs-type">Method</span> <span class="hljs-variable">method</span> <span class="hljs-operator">=</span> methodSignature.getMethod();
            <span class="hljs-type">EagleEye</span> <span class="hljs-variable">eagleEye</span> <span class="hljs-operator">=</span> method.getAnnotation(EagleEye.class);
            <span class="hljs-type">String</span> <span class="hljs-variable">desc</span> <span class="hljs-operator">=</span> eagleEye.desc();
            log.info(<span class="hljs-string">"============请求开始=========="</span>);
            log.info(<span class="hljs-string">"请求链接:{}"</span>, request.getRequestURI().toString());
            log.info(<span class="hljs-string">"接口描述:{}"</span>, desc);
            log.info(<span class="hljs-string">"请求类型:{}"</span>, request.getMethod());
            log.info(<span class="hljs-string">"请求方法:{}.{}"</span>, signature.getDeclaringTypeName(), signature.getName());
            log.info(<span class="hljs-string">"请求IP:{}"</span>, request.getRemoteAddr());
            log.info(<span class="hljs-string">"请求入参:{}"</span>, JSON.toJSONString(pjp.getArgs()));
            <span class="hljs-type">Object</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> pjp.proceed();
            <span class="hljs-type">long</span> <span class="hljs-variable">end</span> <span class="hljs-operator">=</span> System.currentTimeMillis();
            log.info(<span class="hljs-string">"请求耗时:{}ms"</span>, end - begin);
            log.info(<span class="hljs-string">"请求返回:{}"</span>, JSON.toJSONString(result));
            log.info(<span class="hljs-string">"=============请求结束==========="</span>);
            <span class="hljs-keyword">return</span> result;
        }
    }

在 @Pointcut 里通过 @annotation 来配置切点,代表我们的 AOP 切面会切到所有用 EagleEye 注解修饰的类。

然后使用 @Around 环绕通知在被注解的方法前后执行一些代码

Object result = pjp.proceed();

这行代码之前就是执行目标方法之前需要执行的代码 ,这行代码之后就是执行目标方法之后需要执行的代码

3. 注解的使用

只需要在需要被注解的方法上面使用自己的注解就行了 这里拿我自己项目中的一个 Controller 中的方法举例

@EagleEye(desc = "分页查询实验")
@GetMapping("/experiment")
@ApiOperation("分页查询实验")
public RsBody<Page<ExperimentVO2>> pageExperiment(ExperimentQueryDTO queryDTO) {
    log.info("请求分页查询实验的方法 pageExperiment, 请求参数为 {}", queryDTO.toString());
    RsBody<Page<ExperimentVO2>> rsBody = new RsBody<>();
    IPage<Experiment> page = experimentV2Service.page(new Page<>(queryDTO.getCurrent() - 1, queryDTO.getSize()), new LambdaQueryWrapper<Experiment>()
    .like(queryDTO.getExperimentId() != null, Experiment::getExperimentId, queryDTO.getExperimentId())
    .eq(queryDTO.getExperimentStatus() != null, Experiment::getExperimentStatus, queryDTO.getExperimentStatus())
    .between(queryDTO.getStartTime() != null && queryDTO.getEndTime() != null, Experiment::getStartTime, queryDTO.getStartTime(), queryDTO.getEndTime())
    .orderBy(true, false, Experiment::getExperimentId));
<span class="hljs-comment">//组装Vo</span>
List&lt;ExperimentVO2&gt; experimentVOList = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span>&lt;&gt;();
<span class="hljs-keyword">for</span> (Experiment experiment : page.getRecords()) {
    <span class="hljs-type">ExperimentVO2</span> <span class="hljs-variable">experimentVO</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ExperimentVO2</span>();
    experimentVO.setExperimentId(experiment.getExperimentId());
    <span class="hljs-type">PlantInfo</span> <span class="hljs-variable">byPlantId</span> <span class="hljs-operator">=</span> plantService.findByPlantId(experiment.getPlantId());
    <span class="hljs-keyword">if</span> (byPlantId != <span class="hljs-literal">null</span>) {
        experimentVO.setPlantName(byPlantId.getPlantName());
    } <span class="hljs-keyword">else</span> {
        experimentVO.setPlantName(<span class="hljs-string">"植物被删除"</span>);
    }
    experimentVO.setStartTime(experiment.getStartTime());
                experimentVO.setEndTime(experiment.getEndTime());
    experimentVO.setExperimentPurpose(experiment.getExperimentPurpose());
    experimentVO.setExperimentDescription(experiment.getExperimentDescription());
    experimentVO.setExperimentAddress(experiment.getExperimentAddress());
    experimentVO.setExperimentPersonName(userService.findById(experiment.getExperimentPersonId()).getUserName());
    experimentVO.setCronType(experiment.getCronType());
    experimentVO.setExperimentStatus(experiment.getExperimentStatus());
    experimentVO.setExperimentResult(experiment.getExperimentResult());
    experimentVOList.add(experimentVO);
}
Page&lt;ExperimentVO2&gt; pageVo = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Page</span>&lt;ExperimentVO2&gt;();
pageVo.setPages(page.getPages());
pageVo.setRecords(experimentVOList);
pageVo.setTotal(page.getTotal());
pageVo.setSize(page.getSize());
pageVo.setCurrent(page.getCurrent());
<span class="hljs-keyword">return</span> rsBody.setBody(<span class="hljs-literal">true</span>).setData(pageVo);

}

4. 测试情况

好的 万事俱备 让我们运行一下程序 并访问这个方法 (过程略过)

测试情况

很有意思吧 ~~


__EOF__

  • 本文作者: 皮皮松
  • 本文链接: https://www.cnblogs.com/songsongsun/p/14517977.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。