Spring AOP 詳解
- 2021 年 5 月 10 日
- 筆記
- Spring AOP
Spring 有三大核心思想,其目的都是為了解耦。
我們日常開發中總能不知不覺用到其中兩種,分別是控制反轉(Inversion of Control, IOC)和依賴注入(Dependency Injection, DI)。
而面向切面編程(Aspect Oriented Programming, AOP)卻時常被人所忽略,但它的作用卻不可忽視。
AOP 的實現的目標是保證開發者在不修改原程式碼的前提下,去為系統中的業務組件添加某種通用功能。
在使用AOP時,我們能常常聽到以下術語:Aspect、Pointcut、Advice、JoinPoint、Weaving。
- Pointcut:切點,表示一組JoinPoint,它定義了Advice將要發生的地方,
- JoinPoint:連接點,表示程式執行過程中能夠插入切面的點,可以是方法的調用或異常的拋出。
- Advice:增強,包括處理時機和處理內容。通俗的將就是什麼時候該做什麼事。
- Aspect:切面,由同一類Pointcut和Advice組成。
- Weaving:織入,就是通過動態代理,在目標對象中執行處理內容的過程。
如果術語描述的不太明白,請允許我在介紹AOP之前講一個故事:
在很久很久以前,有一個國家叫M國,其國力昌盛,但皇帝大限將至,便將皇位傳給太子。
不久後,皇帝駕崩,太子正式登基。但太子念其國力昌盛,整日無所作為,朝事荒廢。
十幾年後,邊境事犯,皇帝欲選取文武兼備的人率軍去平定叛亂,但此時朝廷人才寥落,只有一人能擔此任。
皇帝為了犒勞該將軍,在其出征之前,將其親子委以朝廷命官,輔佐在自己身邊。
經過數年苦戰,該將軍榮耀而歸,平定叛亂。皇帝高興萬分,賞其千金,封為萬戶侯。
經此事後,皇帝重整朝廷,不久後,國家便重回巔峰。
聽完如上故事,請思考並類比術語的含義:
Pointcut:文武兼備的人
JoinPoint:上文中的將軍
Advice:將軍出征前皇帝留任其親子在身邊、將軍回來後皇帝對其進行賞賜。
Aspect:文武兼備的人出征這個事件
Weaving:對文武兼備的人出征前後乾的事的過程。
本故事只能使你理解它這些術語的含義,並不能描述AOP的目標,也就是解耦,相信解耦大家都清除,這裡就不說了。
接下來開始詳細介紹AOP的概念以及使用,首先介紹Pointcut,下面是一段官方介紹:
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
上面的例子定義了一個名為’anyOldTransfer’的切入點,它將匹配任何名為’transfer’的方法的執行。
Spring AOP支援以下AspectJ切入點指示符(PCD),用於切入點表達式中:
| 切入點指示符 | 描述 |
| execution | 限制匹配連接點的方法 |
| within | 限制匹配連接點的包或者類 |
| @within | 限制匹配連接點的類帶有指定註解 |
| arg | 限制匹配連接點的參數類型 |
| @args | 限制匹配連接點的參數帶有指定註解 |
| target | 限制匹配連接點目標對象的類型 |
| @target | 與@within的功能類似,但註解的保留策略須為RUNTIME |
| this | 限制匹配連接點的AOP代理類的類型 |
| @annotation | 限制匹配連接點的方法帶有指定的註解 |
| bean | 限制連接點是指定的Bean,或一組命名Bean(使用通配符時) |
看完上述切點表達式不理解的話很正常,下面給出詳細介紹:
1、@Pointcut是創建切入點,切入點不用寫程式碼,返回類型為void。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
execution(方法修飾符(可選) 返回類型 類路徑 方法名 參數 異常模式(可選))
- 修飾符匹配(modifiers-pattern?)
- 返回值匹配(ret-type-pattern)
- 類路徑匹配(declaring-type-pattern?)
- 方法名匹配(name-pattern)
- 參數匹配(param-pattern),可以指定具體參數類型,多個參數用”,”隔開。
- 異常類型匹配(throws-pattern?)
- 以上匹配中後面跟著”?”的表示是可選項。
多個匹配之間我們可以使用鏈接符 &&、||、!來表示 「且」、「或」、「非」的關係。
但是在使用 XML 文件配置時,這些符號有特殊的含義,所以我們使用 「and」、「or」、「not」來表示。
示例:
// 匹配AccountService的任意方法 execution(* com.xyz.service.AccountService.*(..))
// 匹配服務包下的任意方法 execution(* com.xyz.service.*.*(..))
//匹配服務包或其子包下的任意方法 execution(* com.xyz.service..*.*(..))
// 匹配位於service包下任意類型 within(com.xyz.service.*)
// 匹配代理實現AccountSercice介面的任意類 this(com.xyz.service.AccountService)
// 匹配目標對象實現AccountService介面的任意類 target(com.xyz.service.AccountService)
2、Advice是增強通知,其有五種通知類型,分別如下:
- @Before,在目標方法調用前執行
- @After,在目標方法調用後執行
- @AfterReturning,在目標方法返回後調用
- @AfterThrowing,在目標方法拋出異常後調用
- @Around,將目標方法封裝起來
首先介紹一下@AfterReturning,官網帶參數示例介紹如下:
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @Aspect public class AfterReturningExample { @AfterReturning( pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } }
returning屬性中使用的名稱必須與通知方法中的參數名稱相對應。
當方法執行返回時,該返回值將作為相應的參數值傳遞到通知方法。
另外returning子句也限制了只能匹配返回指定類型的值,如果是Object類型,將可以匹配任何返回值。
其次是@AfterThrowing的帶參數示例:
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @Aspect public class AfterThrowingExample { @AfterThrowing( pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } }
throwing屬性中使用的名稱必須和通知方法中的參數名稱相對應。
當方法拋出異常時,該異常將作為相應的參數傳遞給通知方法。
另外throwing子句也限制了只能匹配到指定異常類型。
接下來介紹@Around示例:
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.ProceedingJoinPoint; @Aspect public class AroundExample { @Around("com.xyz.myapp.SystemArchitecture.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } }
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
@Around可以在方法執行之前和之後工作,並能決定該方法何時執行以及如何調用。
該通知方法的第一個參數必須為ProceedingJoinPoint類型。
在通知方法中,調用ProceedingJoinPoint的proceed方法來執行匹配的方法。
proceed方法也可能被調用解析Object[]數組,它被用於匹配方法執行的參數。
通知方法返回的值將是匹配方法調用者看到的返回值。
最後,任何通知方法都可以將JoinPoint作為它的第一個參數,@Around除外,它第一個參數必須是ProceedingJoinPoint。
JoinPoint可以提供許多有用的參數,比如getArgs、getThis、getTarget等等。
目前我們已經看到了如何綁定返回值或異常值。如果要使參數用於通知接收,可以使用綁定形式的args。
如果在args表達式中使用參數名替代類型名稱,則在調用通知方法時,將相應參數的值作為參數值傳遞就可以了。
示例如下:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
arg(account,..)切入點表達式有兩個作用,第一它將匹配限制為方法需要有至少一個參數。
第二它限制了參數的類型為Account,並且使該對象可以用於通知方法。
另外它也可以用如下方式表示:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
代理對象、目標對象、和註解等都可以像如下示例使用:
首先定義一個註解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Auditable { AuditCode value(); }
然後是與@Auditable相匹配的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
通知參數和泛型:(不適用於集合泛型)
public interface Sample<T> { void sampleGenericMethod(T param); void sampleGenericCollectionMethod(Collection<T> param); }
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
通知方法中的參數綁定依賴於切入點表達式中使用的名稱。
因為參數名稱無法通過java反射活動,因此Sping AOP使用如下策略確定參數名稱:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames="bean,auditable") public void audit(Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code and bean }
如果通知方法第一個參數是JoinPoint,ProceedingJoinPoint等,則可以省去參數。
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
JoinPoint、ProceedingJoinPoint類型特別方便,可以通過它的getArgs方法獲取參數。
當多個通知都希望在同一連接點運行時,除非另外指定,否則執行順序是不確定的。
可以通過org.springframework.core.Ordered的Order註解來確定順序,值越低,優先順序越高。
3、引入(Introductions)

這個註解感覺用起來作用不大,就不介紹了。

