Spring AOP中定義切點(PointCut)和通知(Advice)

本文討論一下Spring AOP編程中的兩個關鍵問題,定義切點和定義通知,理解這兩個問題能應付大部分AOP場景。

如果你還不熟悉AOP,請先看AOP基本原理,本文的例子也沿用了AOP基本原理中的例子。

切點表達式

切點的功能是指出切面的通知應該從哪裡織入應用的執行流。切面只能織入公共方法。

在Spring AOP中,使用AspectJ的切點表達式語言定義切點其中excecution()是最重要的描述符,其它描述符用於輔助excecution()。

excecution()的語法如下

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)n

這個語法看似複雜,但是我們逐個分解一下,其實就是描述了一個方法的特徵:

問號表示可選項,即可以不指定。

excecution(* com.tianmaying.service.BlogService.updateBlog(..))

  • modifier-pattern:表示方法的修飾符
  • ret-type-pattern:表示方法的返回值
  • declaring-type-pattern?:表示方法所在的類的路徑
  • name-pattern:表示方法名
  • param-pattern:表示方法的參數
  • throws-pattern:表示方法拋出的異常

注意事項

  • 其中後面跟著「?」的是可選項。
  • 在各個pattern中,可以使用"*"來表示匹配所有。
  • 在param-pattern中,可以指定具體的參數類型,多個參數間用「,」隔開,各個也可以用「*」來表示匹配任意類型的參數,如(String)表示匹配一個String參數的方法;(*,String)表示匹配有兩個參數的方法,第一個參數可以是任意類型,而第二個參數是String類型。
  • 可以用(..)表示零個或多個任意的方法參數。

使用&&符號表示與關係,使用||表示或關係、使用!表示非關係。在XML文件中使用and、or和not這三個符號。

在切點中引用Bean

Spring還提供了一個bean()描述符,用於在切點表達式中引用Spring Beans。例如:

excecution(* com.tianmaying.service.BlogService.updateBlog(..)) and bean(tianmayingBlog)n

這表示將切面應用於BlogService的updateBlog方法上,但是僅限於ID為tianmayingBlog的Bean。

也可以排除特定的Bean:

excecution(* com.tianmaying.service.BlogService.updateBlog(..)) and !bean(tianmayingBlog)n

其它切點描述符

其它可用的描述符包括:

  • args()

  • @args()

  • execution()

  • this()

  • target()

  • @target()

  • within()

  • @within()

  • @annotation

當你有更加複雜的切點需要描述時,你可能可以用上這些描述符,通過這些你可以設置目標類實現的介面、方法和類擁有的標註等信息。具體可以參考Spring的官方文檔。

這裡一共有9個描述符,execution()前面已經詳細討論過,其它幾個可以做一個簡單的分類:

  • this()是用來限定方法所屬的類,比如this(com.tianmaying.service.BlogServiceInterface)表示實現了com.tianmaying.service.BlogServiceInterface的所有類。如果this括弧內是具體類而不是介面的話,則表示單個類。

  • @annotation表示具有某個標註的方法,比如@annotation(org.springframework.transaction.annotation.Transactional)表示被Transactional標註的方法

  • args 表示方法的參數屬於一個特定的類

  • within 表示方法屬於一個特定的類

  • target 表示方法所屬的類

  • 它們對應的加了@的版本則表示對應的類具有某個標註。

單獨定義切點

詳細了解了定義切點之後,在回顧上一節中的代碼:

package com.tianmaying.aopdemo.aspect;nnimport org.aspectj.lang.annotation.AfterReturning;nimport org.aspectj.lang.annotation.Aspect;nimport org.aspectj.lang.annotation.Pointcut;nimport org.springframework.stereotype.Component;nn@Aspect //1n@Componentnpublic class LogAspect {nn @Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))") //2n private void logPointCut() {n }nn @AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3n public void logBookingStatus(boolean retVal) { //4n if (retVal) {n System.out.println("booking flight succeeded!");n } else {n System.out.println("booking flight failed!");n }n }n}n

可以看到通過標註方式定義切點只需要兩個步驟:

  1. 定義一個空方法
  2. 使用@Piontcut標註,填入切點表達式

@AfterReturning(pointcut = "execution(* com.tianmaying.aopdemo..*.bookFlight(..))", returning = "retVal")中通過pointcout = "logPointCut"引用了這個切點。當然也可以在@AfterReturning()直接定義切點表達式,如:

@AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3n

推薦使用前一種方法,因為這樣可以在多個通知中復用切點的定義。

切點定義實例

這裡我們給出一些切點的定義實例。

@Pointcut("execution(public * *(..))") // 1nprivate void anyPublicOperation() {}nn@Pointcut("within(com.xyz.someapp.web..*))") // 2nprivate void inTrading() {}nn@Pointcut("anyPublicOperation() && inTrading()") // 3nprivate void tradingOperation() {}nn@within(org.springframework.transaction.annotation.Transactional) // 4nprivate void transactionalClass() {}nn@annotation(org.springframework.transaction.annotation.Transactional) //5nprivate void transactionalMethod() {}n

上面的代碼定義了三個切點:

  1. 任意公共方法(實際應用中一般不會定義這樣的切點)
  2. 在within(com.xyz.someapp.web包或者其子包下任意類的方法
  3. 同時滿足切點1和切點2條件的切點,這裡使用了&&符號
  4. 標註了Transactional的類的方法
  5. 標註了Transactional的方法

定義通知

依然回到TimeRecordingAspect的代碼:

@Aspectn@Componentnpublic class TimeRecordingAspect {nn @Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))")n private void timeRecordingPointCut() {n }nn @Around("timeRecordingPointCut()") //1n public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //2nn long start = System.currentTimeMillis();n Object retVal = pjp.proceed(); // 3nn long duration = System.currentTimeMillis() - start;n System.out.println(String.format(n "time for booking flight is %d seconds", duration));nn return retVal;n }n}n

定義了切點之後,我們需要定義何時調用recordTime方法記錄時間,即需要定義通知。

AspectJ提供了五種定義通知的標註:

  • @Before:前置通知,在調用目標方法之前執行通知定義的任務
  • @After:後置通知,在目標方法執行結束後,無論執行結果如何都執行通知定義的任務
  • @After-returning:後置通知,在目標方法執行結束後,如果執行成功,則執行通知定義的任務
  • @After-throwing:異常通知,如果目標方法執行過程中拋出異常,則執行通知定義的任務
  • @Around:環繞通知,在目標方法執行前和執行後,都需要執行通知定義的任務

通過標註定義通知只需要兩個步驟:

  1. 將以上五種標註之一添加到切面的方法中
  2. 在標註中設置切點的定義

創建環繞通知

環繞通知相比其它四種通知有其特殊之處。環繞通知本質上是將前置通知、後置通知和異常通知整合成一個單獨的通知。

用@Around標註的方法,該方法必須有一個ProceedingJoinPoint類型的參數,比如上面代碼中的recordTime的簽名:

public Object recordTime(ProceedingJoinPoint pjp) throws Throwablen

在方法體中,需要通過這個參數,以joinPoint.proceed();的形式調用目標方法。注意在環繞通知中必須進行該調用,否則目標方法本身的執行就會被跳過。

比如在recoredTime的實現中:

long start = System.currentTimeMillis();nObject retVal = pjp.proceed();nlong duration = System.currentTimeMillis() - start;nSystem.out.println(String.format("time for booking flight is %d seconds", duration));n

在目標方法調用前首先記錄系統時間,然後通過pjp.proceed()調用目標方法,調用完之後再次記錄系統時間,即可計算出目標方法的耗時。

處理通知中參數

有時我們需要給通知中的方法傳遞目標對象的一些信息,比如傳入目標業務方法的參數。

在前面的代碼中我們曾經通過@AfterReturning(pointcut = "logPointCut()", returning = "retVal")在通知中獲取目標業務方法的返回值。獲取參數的方式則需要使用關鍵詞是args。

假設需要對系統中的accountOperator方法,做Account的驗證,驗證邏輯以切面的方式顯示,示例如下:

@Before("com.tianmaying.UserService.accountOperator() && args(account,..)")npublic void validateAccount(Account account) {n // ...n // 這可以獲取傳入accountOperator中的Account信息n}n

args()中參數的名稱必須跟切點方法的簽名中(public void validateAccount(Account account))的參數名稱相同。如果使用切點函數定義,其中的參數名稱也必須與通知方法簽名中的參數完全相同,例如:

@Pointcut("com.tianmaying.UserService.accountOperator() && args(account,..)")nprivate void accountOperation(Account account) {}nn@Before("accountOperation(account)")npublic void validateAccount(Account account) {n // ...n}n

小節

AOP的知識就介紹到這裡,更複雜的場景還需要了解AOP更深入的一些知識,比如:

  • AOP的生成代理的方式
  • 多個切面的順序
  • 更複雜的參數類型(如泛型)
  • 使用AspectJ的切面
  • ...

感興趣的同學可以繼續深入學習,最好的學習材料就是Spring的官方文檔。

天碼營外圍的網站開發的也基本只使用了我們介紹的這些知識點,可見這些關鍵知識點足以解決大部分複雜場景,確實需要用到更高級的特性時,再去參考文檔即可。

歡迎關注天碼營微信公眾號: TMY-EDU

小編重點推薦:

Spring MVC實戰入門訓練

Java貪吃蛇的設計與實現

一起來寫網易雲音樂Java爬蟲

Spring Data JPA實戰入門訓練

Java Web實戰訓練

更多精彩內容請訪問天碼營網站

推薦閱讀:

spring-jdbc 目前還是一個主流的廣泛使用的持久化框架嗎?
關於Spring MVC的教程和例子?
Spring boot與Spring cloud 是什麼關係?
springboot怎麼學?

TAG:Spring | AOP | SpringBoot |