1. 程式人生 > >使用自定義註解和切面AOP實現Java程式增強

使用自定義註解和切面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介面中有一行註釋如此寫到: ```java /** * The common interface extended by all annotation types. * ... */ public interface Annotation {...} ``` 這說明其他註解都擴充套件自 `Annotation` 這個介面,也就是說註解的本質就是一個介面。 以 Spring Boot 中的一個註解為例: ```java @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Indexed public @interface Component { String value() default ""; } ``` 它實際上相當於: ```java public interface Component extends Annotation{...} ``` 而`@interface` 可以看成是一個語法糖。 ### 1.2註解的要素 依然來看 `@Component` 這個例子: ```java @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Indexed public @interface Component { String value() default ""; } ``` 在註解定義上有幾個註解`@Target, @Retention, @Documented`,被稱為 **元註解**。 所謂元註解就是說明註解的註解 `Java` 中的元註解共有以下幾個: #### 1.2.1 @Target `@Target`顧名思義,這個註解標識了被修飾註解的作用物件。我們看看它的原始碼: ```java @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`這個列舉之中: ```java 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` 註解指定了被修飾的註解的生命週期。定義如下: ```java @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { /** * Returns the retention policy. * @return the retention policy */ RetentionPolicy value(); } ``` 可以看到這個註解帶一個 `RetentionPolicy` 的列舉值: ```java public enum RetentionPolicy { SOURCE, CLASS, RUNTIME } ``` - `SOURCE` 表示註解編譯時可見,編譯完後就被丟棄。這種註解一般用於在編譯器做一些事情; - `CLASS` 表示在編譯完後寫入 class 檔案,但在類載入後被丟棄。這種註解一般用於在類載入階段做一些事情; - `RUNTIME` 則表示註解會一直起作用。 #### 1.2.3 @Documented 這個註解比較簡單,表示是否新增到 `java doc` 中。 #### 1.2.4 @Inherited 這個也比較簡單,表示註解是否被繼承。這個註解不是很常用。 > 注意:元註解只在定義註解時被使用! ### 1.3 註解的構成 從上面的元註解可以瞭解到,一個註解可以關聯多個 `ElementType`,但只能有一個 `RetentionPolicy`: ![註解的構成](https://img2020.cnblogs.com/blog/2256920/202103/2256920-20210311145703990-1886515788.png) Java 中有三個常用的內建註解,其實相信大家都用過或者見過。不過在瞭解了註解的真實面貌以後,不妨重新認識一下吧! ### 1.4 Java內建註解 #### 1.4.1 @Override `@Override`它的定義為: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { } ``` 可見這個註解沒有任何取值,只能修飾方法,而且RetentionPolicy 為 SOURCE,說明這是一個僅在編譯階段起作用的註解。 它的真實作用想必大家一定知道,就是在編譯階段,如果一個類的方法被 `@Override` 修飾,編譯器會在其父類中查詢是否有同簽名函式,如果沒有則編譯報錯。可見這確實是一個除了在編譯階段就沒什麼用的註解。 #### 1.4.2 @Deprecated `@Deprecated`它的定義為: ```java @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { } ``` 這個註解也沒有任何取值,能修飾所有的型別,永久存在。這個註解的作用是,告訴使用者被修飾的程式碼不推薦使用了,可能會在下一個軟體版本中移除。這個註解僅僅起到一個通知機制,如果程式碼呼叫了被@Deprecated 修飾的程式碼,編譯器在編譯時輸出一個編譯告警。 #### 1.4.3 @SuppressWarnings `@SuppressWarnings`它的定義為: ```java @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 not
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. * *

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(); } ``` 這個註解有一個字串陣列的值,需要我們使用註解的時候傳遞。可以在型別、屬性、方法、引數、建構函式和區域性變數前使用,宣告週期是編譯期。 這個註解的主要作用是壓制編譯告警的。 ## 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的) ```java /** * @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.首先定義一個註解,程式碼如下 ```java /** * @Author Song * @Date 2020/5/26 9:44 * @Version 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface EagleEye { /** * @Retention(RetentionPolicy.RUNTIME) * 定義了註解的生命週期為執行時 *

* @Target(ElementType.METHOD) * 定義了註解的作用域為方法 *

* Documented * 標識該註解可以被JavaDoc記錄 *

* 定義註解名稱為EagleEye(鷹眼,哈哈~~) *

* 定義一個元素desc,用來描述被修飾的方法 *

* 介面描述 * * @return */ String desc() default ""; } ``` ### 2.定義切片內並寫好自己想要增強的方法 直接貼程式碼了~~ ```java /** * @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中的方法舉例 ```java @EagleEye(desc = "分頁查詢實驗") @GetMapping("/experiment") @ApiOperation("分頁查詢實驗") public RsBody> pageExperiment(ExperimentQueryDTO queryDTO) { log.info("請求分頁查詢實驗的方法pageExperiment,請求引數為{}", queryDTO.toString()); RsBody> rsBody = new RsBody<>(); IPage page = experimentV2Service.page(new Page<>(queryDTO.getCurrent() - 1, queryDTO.getSize()), new LambdaQueryWrapper() .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 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 pageVo = new Page(); 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.測試情況 好的 萬事俱備 讓我們執行一下程式 並訪問這個方法 (過程略過) ![測試情況](https://img2020.cnblogs.com/blog/2256920/202103/2256920-20210311145721308-119075180.png) 很有意