您現在的位置是:首頁 > 綜合

會使用自定義註解 ≈ 好的程式設計師?教你結合 AOP 切面列印請求日誌

  • 由 程式設計菌zfn 發表于 綜合
  • 2022-03-26
簡介2 第二步使用切面註解進行標記,因為是對請求相關的日誌列印,所以我們隨便寫一個控制層介面方法進行測試:** * @description: TODO * @author: HUALEI * @date: 2021-11-24 * @tim

宣告有什麼作用

一、前言

今天就帶著大夥梳理一遍註解也就是 @interface 正確的開啟方式,除此之外,結合 AOP 切面統一打印出入參日誌,對於每個訪問註解繫結的介面方法的請求都一目瞭然,不僅方便介面的除錯,還能給你一個優雅、整齊且大方的控制檯日誌記錄。

二、效果演示

2。1 訪問介面

會使用自定義註解 ≈ 好的程式設計師?教你結合 AOP 切面列印請求日誌

2。2 控制檯日誌輸出

會使用自定義註解 ≈ 好的程式設計師?教你結合 AOP 切面列印請求日誌

三、如何設計一個註解

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 使用流程

會使用自定義註解 ≈ 好的程式設計師?教你結合 AOP 切面列印請求日誌

四、程式碼實現

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

最後一步也是最關鍵的一步,在執行時解析註解執行切面操作,所以對應地寫一個切面類:

會使用自定義註解 ≈ 好的程式設計師?教你結合 AOP 切面列印請求日誌

新建切面類後,考慮到日誌的列印,這段程式碼必不可少:

/** * @description: TODO * @author: HUALEI * @date: 2021-11-19 * @time: 15:56 */@Aspect@Componentpublic 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 傳入的新的引數去執行目標方法

整個程式碼都有註解,這裡就不贅述程式碼邏輯了!

會使用自定義註解 ≈ 好的程式設計師?教你結合 AOP 切面列印請求日誌

4。4 擴充套件

除了上面用到的 @PointCut 和 @Around 註解,還有另外

4

個使用 AOP 常用的註解:

@Before :前置增強,在切點之前織入相關程式碼

@After :final 增強,不管是丟擲異常或者正常退出都會執行

@AfterReturning :後置增強,方法正常退出時執行

@AfterThrowing :異常丟擲增強,切點方法丟擲異常時執行

執行順序:@Around => @Before => 執行介面方法中的程式碼 => @After => @AfterReturning

有興趣的同學,可以環繞增強中的程式碼拆分到前置和後置增強中,以便更好地理解這四個常用註解使用場景! (๑•̀ㅂ•́)و✧

作者:HUALEI

連結:https://juejin。cn/post/7034406941925474318

Top