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?)
這個語法看似複雜,但是我們逐個分解一下,其實就是描述了一個方法的特徵:
問號表示可選項,即可以不指定。
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')
這表示將切面應用於BlogService
的updateBlog
方法上,但是僅限於ID為tianmayingBlog的Bean。
也可以排除特定的Bean:
excecution(* com.tianmaying.service.BlogService.updateBlog(..)) and !bean('tianmayingBlog')
其它切點描述符
其它可用的描述符包括:
-
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;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //1
@Component
public class LogAspect {
@Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))") //2
private void logPointCut() {
}
@AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3
public void logBookingStatus(boolean retVal) { //4
if (retVal) {
System.out.println("booking flight succeeded!");
} else {
System.out.println("booking flight failed!");
}
}
}
可以看到通過標註方式定義切點只需要兩個步驟:
- 定義一個空方法
- 使用
@Piontcut
標註,填入切點表示式
@AfterReturning(pointcut = "execution(* com.tianmaying.aopdemo..*.bookFlight(..))", returning = "retVal")
中通過pointcout = "logPointCut"
引用了這個切點。當然也可以在@AfterReturning()
直接定義切點表示式,如:
@AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3
推薦使用前一種方法,因為這樣可以在多個通知中複用切點的定義。
切點定義例項
這裡我們給出一些切點的定義例項。
@Pointcut("execution(public * *(..))") // 1
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.web..*))") // 2
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()") // 3
private void tradingOperation() {}
@within(org.springframework.transaction.annotation.Transactional) // 4
private void transactionalClass() {}
@annotation(org.springframework.transaction.annotation.Transactional) //5
private void transactionalMethod() {}
上面的程式碼定義了三個切點:
- 任意公共方法(實際應用中一般不會定義這樣的切點)
- 在
within(com.xyz.someapp.web
包或者其子包下任意類的方法 - 同時滿足切點1和切點2條件的切點,這裡使用了
&&
符號 - 標註了
Transactional
的類的方法 - 標註了
Transactional
的方法
定義通知
依然回到TimeRecordingAspect
的程式碼:
@Aspect
@Component
public class TimeRecordingAspect {
@Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))")
private void timeRecordingPointCut() {
}
@Around("timeRecordingPointCut()") //1
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //2
long start = System.currentTimeMillis();
Object retVal = pjp.proceed(); // 3
long duration = System.currentTimeMillis() - start;
System.out.println(String.format(
"time for booking flight is %d seconds", duration));
return retVal;
}
}
定義了切點之後,我們需要定義何時呼叫recordTime
方法記錄時間,即需要定義通知。
AspectJ提供了五種定義通知的標註:
@Before
:前置通知,在呼叫目標方法之前執行通知定義的任務@After
:後置通知,在目標方法執行結束後,無論執行結果如何都執行通知定義的任務@After-returning
:後置通知,在目標方法執行結束後,如果執行成功,則執行通知定義的任務@After-throwing
:異常通知,如果目標方法執行過程中丟擲異常,則執行通知定義的任務@Around
:環繞通知,在目標方法執行前和執行後,都需要執行通知定義的任務
通過標註定義通知只需要兩個步驟:
- 將以上五種標註之一新增到切面的方法中
- 在標註中設定切點的定義
建立環繞通知
環繞通知相比其它四種通知有其特殊之處。環繞通知本質上是將前置通知、後置通知和異常通知整合成一個單獨的通知。
用@Around
標註的方法,該方法必須有一個ProceedingJoinPoint
型別的引數,比如上面程式碼中的recordTime
的簽名:
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable
在方法體中,需要通過這個引數,以joinPoint.proceed();
的形式呼叫目標方法。注意在環繞通知中必須進行該呼叫,否則目標方法本身的執行就會被跳過。
比如在recoredTime
的實現中:
long start = System.currentTimeMillis();
Object retVal = pjp.proceed();
long duration = System.currentTimeMillis() - start;
System.out.println(String.format("time for booking flight is %d seconds", duration));
在目標方法呼叫前首先記錄系統時間,然後通過pjp.proceed()
呼叫目標方法,呼叫完之後再次記錄系統時間,即可計算出目標方法的耗時。
處理通知中引數
有時我們需要給通知中的方法傳遞目標物件的一些資訊,比如傳入目標業務方法的引數。
在前面的程式碼中我們曾經通過@AfterReturning(pointcut = "logPointCut()", returning = "retVal")
在通知中獲取目標業務方法的返回值。獲取引數的方式則需要使用關鍵詞是args
。
假設需要對系統中的accountOperator
方法,做Account
的驗證,驗證邏輯以切面的方式顯示,示例如下:
@Before("com.tianmaying.UserService.accountOperator() && args(account,..)")
public void validateAccount(Account account) {
// ...
// 這可以獲取傳入accountOperator中的Account資訊
}
args()
中引數的名稱必須跟切點方法的簽名中(public void validateAccount(Account account)
)的引數名稱相同。如果使用切點函式定義,其中的引數名稱也必須與通知方法簽名中的引數完全相同,例如:
@Pointcut("com.tianmaying.UserService.accountOperator() && args(account,..)")
private void accountOperation(Account account) {}
@Before("accountOperation(account)")
public void validateAccount(Account account) {
// ...
}
小節
AOP的知識就介紹到這裡,更復雜的場景還需要了解AOP更深入的一些知識,比如:
- AOP的生成代理的方式
- 多個切面的順序
- 更復雜的引數型別(如泛型)
- 使用AspectJ的切面
- ...
感興趣的同學可以繼續深入學習,最好的學習材料就是Spring的官方文件。
天碼營外圍的網站開發的也基本只使用了我們介紹的這些知識點,可見這些關鍵知識點足以解決大部分複雜場景,確實需要用到更高階的特性時,再去參考文件即可。