javascript
Spring AOP中定义切点(PointCut)和通知(Advice)
本文討論一下Spring AOP編程中的兩個(gè)關(guān)鍵問(wèn)題,定義切點(diǎn)和定義通知,理解這兩個(gè)問(wèn)題能應(yīng)付大部分AOP場(chǎng)景。
如果你還不熟悉AOP,請(qǐng)先看AOP基本原理,本文的例子也沿用了AOP基本原理中的例子。
切點(diǎn)表達(dá)式
切點(diǎn)的功能是指出切面的通知應(yīng)該從哪里織入應(yīng)用的執(zhí)行流。切面只能織入公共方法。
在Spring AOP中,使用AspectJ的切點(diǎn)表達(dá)式語(yǔ)言定義切點(diǎn)其中excecution()是最重要的描述符,其它描述符用于輔助excecution()。
excecution()的語(yǔ)法如下
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)這個(gè)語(yǔ)法看似復(fù)雜,但是我們逐個(gè)分解一下,其實(shí)就是描述了一個(gè)方法的特征:
問(wèn)號(hào)表示可選項(xiàng),即可以不指定。
excecution(* com.tianmaying.service.BlogService.updateBlog(..))
- modifier-pattern:表示方法的修飾符
- ret-type-pattern:表示方法的返回值
- declaring-type-pattern?:表示方法所在的類的路徑
- name-pattern:表示方法名
- param-pattern:表示方法的參數(shù)
- throws-pattern:表示方法拋出的異常
注意事項(xiàng)
- 其中后面跟著“?”的是可選項(xiàng)。
- 在各個(gè)pattern中,可以使用"*"來(lái)表示匹配所有。
- 在param-pattern中,可以指定具體的參數(shù)類型,多個(gè)參數(shù)間用“,”隔開,各個(gè)也可以用“*”來(lái)表示匹配任意類型的參數(shù),如(String)表示匹配一個(gè)String參數(shù)的方法;(*,String)表示匹配有兩個(gè)參數(shù)的方法,第一個(gè)參數(shù)可以是任意類型,而第二個(gè)參數(shù)是String類型。
- 可以用(..)表示零個(gè)或多個(gè)任意的方法參數(shù)。
使用&&符號(hào)表示與關(guān)系,使用||表示或關(guān)系、使用!表示非關(guān)系。在XML文件中使用and、or和not這三個(gè)符號(hào)。
在切點(diǎn)中引用Bean
Spring還提供了一個(gè)bean()描述符,用于在切點(diǎn)表達(dá)式中引用Spring Beans。例如:
excecution(* com.tianmaying.service.BlogService.updateBlog(..)) and bean('tianmayingBlog')這表示將切面應(yīng)用于BlogService的updateBlog方法上,但是僅限于ID為tianmayingBlog的Bean。
也可以排除特定的Bean:
excecution(* com.tianmaying.service.BlogService.updateBlog(..)) and !bean('tianmayingBlog')其它切點(diǎn)描述符
其它可用的描述符包括:
-
args()
-
@args()
-
execution()
-
this()
-
target()
-
@target()
-
within()
-
@within()
-
@annotation
當(dāng)你有更加復(fù)雜的切點(diǎn)需要描述時(shí),你可能可以用上這些描述符,通過(guò)這些你可以設(shè)置目標(biāo)類實(shí)現(xiàn)的接口、方法和類擁有的標(biāo)注等信息。具體可以參考Spring的官方文檔。
這里一共有9個(gè)描述符,execution()前面已經(jīng)詳細(xì)討論過(guò),其它幾個(gè)可以做一個(gè)簡(jiǎn)單的分類:
-
this()是用來(lái)限定方法所屬的類,比如this(com.tianmaying.service.BlogServiceInterface)表示實(shí)現(xiàn)了com.tianmaying.service.BlogServiceInterface的所有類。如果this括號(hào)內(nèi)是具體類而不是接口的話,則表示單個(gè)類。
-
@annotation表示具有某個(gè)標(biāo)注的方法,比如@annotation(org.springframework.transaction.annotation.Transactional)表示被Transactional標(biāo)注的方法
-
args?表示方法的參數(shù)屬于一個(gè)特定的類
-
within?表示方法屬于一個(gè)特定的類
-
target?表示方法所屬的類
-
它們對(duì)應(yīng)的加了@的版本則表示對(duì)應(yīng)的類具有某個(gè)標(biāo)注。
單獨(dú)定義切點(diǎn)
詳細(xì)了解了定義切點(diǎn)之后,在回顧上一節(jié)中的代碼:
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(..))") //2private void logPointCut() {}@AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3public void logBookingStatus(boolean retVal) { //4if (retVal) {System.out.println("booking flight succeeded!");} else {System.out.println("booking flight failed!");}} }可以看到通過(guò)標(biāo)注方式定義切點(diǎn)只需要兩個(gè)步驟:
@AfterReturning(pointcut = "execution(* com.tianmaying.aopdemo..*.bookFlight(..))", returning = "retVal")中通過(guò)pointcout = "logPointCut"引用了這個(gè)切點(diǎn)。當(dāng)然也可以在@AfterReturning()直接定義切點(diǎn)表達(dá)式,如:
@AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3推薦使用前一種方法,因?yàn)檫@樣可以在多個(gè)通知中復(fù)用切點(diǎn)的定義。
切點(diǎn)定義實(shí)例
這里我們給出一些切點(diǎn)的定義實(shí)例。
@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() {}上面的代碼定義了三個(gè)切點(diǎn):
定義通知
依然回到TimeRecordingAspect的代碼:
@Aspect @Component public class TimeRecordingAspect {@Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))")private void timeRecordingPointCut() {}@Around("timeRecordingPointCut()") //1public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //2long start = System.currentTimeMillis();Object retVal = pjp.proceed(); // 3long duration = System.currentTimeMillis() - start;System.out.println(String.format("time for booking flight is %d seconds", duration));return retVal;} }定義了切點(diǎn)之后,我們需要定義何時(shí)調(diào)用recordTime方法記錄時(shí)間,即需要定義通知。
AspectJ提供了五種定義通知的標(biāo)注:
- @Before:前置通知,在調(diào)用目標(biāo)方法之前執(zhí)行通知定義的任務(wù)
- @After:后置通知,在目標(biāo)方法執(zhí)行結(jié)束后,無(wú)論執(zhí)行結(jié)果如何都執(zhí)行通知定義的任務(wù)
- @After-returning:后置通知,在目標(biāo)方法執(zhí)行結(jié)束后,如果執(zhí)行成功,則執(zhí)行通知定義的任務(wù)
- @After-throwing:異常通知,如果目標(biāo)方法執(zhí)行過(guò)程中拋出異常,則執(zhí)行通知定義的任務(wù)
- @Around:環(huán)繞通知,在目標(biāo)方法執(zhí)行前和執(zhí)行后,都需要執(zhí)行通知定義的任務(wù)
通過(guò)標(biāo)注定義通知只需要兩個(gè)步驟:
創(chuàng)建環(huán)繞通知
環(huán)繞通知相比其它四種通知有其特殊之處。環(huán)繞通知本質(zhì)上是將前置通知、后置通知和異常通知整合成一個(gè)單獨(dú)的通知。
用@Around標(biāo)注的方法,該方法必須有一個(gè)ProceedingJoinPoint類型的參數(shù),比如上面代碼中的recordTime的簽名:
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable在方法體中,需要通過(guò)這個(gè)參數(shù),以joinPoint.proceed();的形式調(diào)用目標(biāo)方法。注意在環(huán)繞通知中必須進(jìn)行該調(diào)用,否則目標(biāo)方法本身的執(zhí)行就會(huì)被跳過(guò)。
比如在recoredTime的實(shí)現(xiàn)中:
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));在目標(biāo)方法調(diào)用前首先記錄系統(tǒng)時(shí)間,然后通過(guò)pjp.proceed()調(diào)用目標(biāo)方法,調(diào)用完之后再次記錄系統(tǒng)時(shí)間,即可計(jì)算出目標(biāo)方法的耗時(shí)。
處理通知中參數(shù)
有時(shí)我們需要給通知中的方法傳遞目標(biāo)對(duì)象的一些信息,比如傳入目標(biāo)業(yè)務(wù)方法的參數(shù)。
在前面的代碼中我們?cè)?jīng)通過(guò)@AfterReturning(pointcut = "logPointCut()", returning = "retVal")在通知中獲取目標(biāo)業(yè)務(wù)方法的返回值。獲取參數(shù)的方式則需要使用關(guān)鍵詞是args。
假設(shè)需要對(duì)系統(tǒng)中的accountOperator方法,做Account的驗(yàn)證,驗(yàn)證邏輯以切面的方式顯示,示例如下:
@Before("com.tianmaying.UserService.accountOperator() && args(account,..)") public void validateAccount(Account account) {// ...// 這可以獲取傳入accountOperator中的Account信息 }args()中參數(shù)的名稱必須跟切點(diǎn)方法的簽名中(public void validateAccount(Account account))的參數(shù)名稱相同。如果使用切點(diǎn)函數(shù)定義,其中的參數(shù)名稱也必須與通知方法簽名中的參數(shù)完全相同,例如:
@Pointcut("com.tianmaying.UserService.accountOperator() && args(account,..)") private void accountOperation(Account account) {}@Before("accountOperation(account)") public void validateAccount(Account account) {// ... }小節(jié)
AOP的知識(shí)就介紹到這里,更復(fù)雜的場(chǎng)景還需要了解AOP更深入的一些知識(shí),比如:
- AOP的生成代理的方式
- 多個(gè)切面的順序
- 更復(fù)雜的參數(shù)類型(如泛型)
- 使用AspectJ的切面
- ...
感興趣的同學(xué)可以繼續(xù)深入學(xué)習(xí),最好的學(xué)習(xí)材料就是Spring的官方文檔。
天碼營(yíng)外圍的網(wǎng)站開發(fā)的也基本只使用了我們介紹的這些知識(shí)點(diǎn),可見這些關(guān)鍵知識(shí)點(diǎn)足以解決大部分復(fù)雜場(chǎng)景,確實(shí)需要用到更高級(jí)的特性時(shí),再去參考文檔即可。
版權(quán)聲明本文由 David創(chuàng)作,轉(zhuǎn)載需署名作者且注明文章出處參考代碼要獲取本文的參考代碼,請(qǐng)?jiān)L問(wèn):?https://www.tianmaying.com/tutorial/spring-aop-point-advice/repo
from:?https://www.tianmaying.com/tutorial/spring-aop-point-advice
總結(jié)
以上是生活随笔為你收集整理的Spring AOP中定义切点(PointCut)和通知(Advice)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JVM GC参数以及GC算法的应用
- 下一篇: Spring Validation(使用