使用自定義注解和切面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 {
    
        @Pointcut("@annotation(com.ctgu.song.plantfactory.v2.annotation.EagleEye)")
        public void eagleEye() {
    
        }
    
    
        @Around("eagleEye()")
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            long begin = System.currentTimeMillis();
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            Signature signature = pjp.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();
            EagleEye eagleEye = method.getAnnotation(EagleEye.class);
            String desc = eagleEye.desc();
            log.info("============請求開始==========");
            log.info("請求鏈接:{}", request.getRequestURI().toString());
            log.info("接口描述:{}", desc);
            log.info("請求類型:{}", request.getMethod());
            log.info("請求方法:{}.{}", signature.getDeclaringTypeName(), signature.getName());
            log.info("請求IP:{}", request.getRemoteAddr());
            log.info("請求入參:{}", JSON.toJSONString(pjp.getArgs()));
            Object result = pjp.proceed();
            long end = System.currentTimeMillis();
            log.info("請求耗時:{}ms", end - begin);
            log.info("請求返回:{}", JSON.toJSONString(result));
            log.info("=============請求結束===========");
            return result;
        }
    }

這邊先不看代碼的具體內容,先簡單介紹一下用到AOP中常用的注解

  • @Aspect : 指定切面類;
  • @Pointcut:公共切入點表達式
  • 通知方法
    • 前置通知(@Before) 目標方法執行之前,執行注解的內容
    • 后置通知(@After)目標方法執行之后,執行注解的內容
    • 返回通知 (@AfterReturning)目標方法返回后,執行注解的內容
    • 異常通知 (@AfterThrowing)目標方法拋出異常后,執行注解的內容
    • 環繞通知 (@Around)目標方法執行前后,分別執行一些代碼

注意 定義好切片類后要將其加入Spring容器內才能使用哦 (可以使用@Component注解)

3. 具體實現(一個例子)

1.首先定義一個注解,代碼如下

        /**
         * @Author Song
         * @Date 2020/5/26 9:44
         * @Version 1.0
         */
        
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.METHOD)
        @Documented
        public @interface EagleEye {
        
            /**
             * @Retention(RetentionPolicy.RUNTIME)
             * 定義了注解的生命周期為運行時
             * <p>
             * @Target(ElementType.METHOD)
             * 定義了注解的作用域為方法
             * <p>
             * Documented
             * 標識該注解可以被JavaDoc記錄
             * <p>
             * 定義注解名稱為EagleEye(鷹眼,哈哈~~)
             * <p>
             * 定義一個元素desc,用來描述被修飾的方法
             * <p>
             * 接口描述
             *
             * @return
             */
            String desc() default "";
        }

2.定義切片內並寫好自己想要增強的方法

直接貼代碼了~~

        /**
         * @Author Song
         * @Date 2020/5/26 9:50
         * @Version 1.0
         */
        @Slf4j
        @Aspect
        @Component
        public class EagleEyeAspect {
        
            @Pointcut("@annotation(com.ctgu.song.plantfactory.v2.annotation.EagleEye)")
            public void eagleEye() {
        
            }
        
        
            @Around("eagleEye()")
            public Object around(ProceedingJoinPoint pjp) throws Throwable {
                long begin = System.currentTimeMillis();
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = attributes.getRequest();
                Signature signature = pjp.getSignature();
                MethodSignature methodSignature = (MethodSignature) signature;
                Method method = methodSignature.getMethod();
                EagleEye eagleEye = method.getAnnotation(EagleEye.class);
                String desc = eagleEye.desc();
                log.info("============請求開始==========");
                log.info("請求鏈接:{}", request.getRequestURI().toString());
                log.info("接口描述:{}", desc);
                log.info("請求類型:{}", request.getMethod());
                log.info("請求方法:{}.{}", signature.getDeclaringTypeName(), signature.getName());
                log.info("請求IP:{}", request.getRemoteAddr());
                log.info("請求入參:{}", JSON.toJSONString(pjp.getArgs()));
                Object result = pjp.proceed();
                long end = System.currentTimeMillis();
                log.info("請求耗時:{}ms", end - begin);
                log.info("請求返回:{}", JSON.toJSONString(result));
                log.info("=============請求結束===========");
                return 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));
        
    //組裝Vo
    List<ExperimentVO2> experimentVOList = new ArrayList<>();
    for (Experiment experiment : page.getRecords()) {
        ExperimentVO2 experimentVO = new ExperimentVO2();
        experimentVO.setExperimentId(experiment.getExperimentId());
        PlantInfo byPlantId = plantService.findByPlantId(experiment.getPlantId());
        if (byPlantId != null) {
            experimentVO.setPlantName(byPlantId.getPlantName());
        } else {
            experimentVO.setPlantName("植物被刪除");
        }
        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<ExperimentVO2> pageVo = new Page<ExperimentVO2>();
    pageVo.setPages(page.getPages());
    pageVo.setRecords(experimentVOList);
    pageVo.setTotal(page.getTotal());
    pageVo.setSize(page.getSize());
    pageVo.setCurrent(page.getCurrent());
    return rsBody.setBody(true).setData(pageVo);
}

4.測試情況

好的 萬事俱備 讓我們運行一下程序 並訪問這個方法 (過程略過)

測試情況

很有意思吧~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM