📢 本文由 gemini-2.5-flash 翻譯
Aspect-Oriented Programming (面向切面程式設計、面向方面程式設計) 是針對特定方法進行程式設計。
動態代理是面向切面程式設計最主流的實作。而 SpringAOP 是 Spring 框架的高階技術,旨在管理 bean 物件的過程中,主要透過底層的動態代理機制,對特定的方法進行程式設計。
統計方法執行時間
匯入依賴
1
2
3
4
| <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
|
撰寫 AOP 程式,針對特定方法根據業務需求進行程式設計
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Slf4j
@Component
@Aspect // AOP類
public class TimeAspect {
// 切入點表達式
@Around("execution(* net.yexca.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long begin = System.currentTimeMillis();
// 呼叫原始方法
Object object = proceedingJoinPoint.proceed();
long end = System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature() + "方法執行時間:{}ms", end-begin);
return object;
}
}
|
AOP 的應用場景有記錄操作日誌、權限控制、交易管理等
核心概念
連結點:JoinPoint,可以被 AOP 控制的方法 (隱含方法執行時的相關資訊)
通知:Advice,指哪些重複的邏輯,也就是共性功能 (最終體現為一個方法)
切入點:PointCut,匹配連結點的條件,通知僅會在切入點方法執行時被應用
切面:Aspect,描述通知與切入點的對應關係 (通知 + 切入點)
目標物件:Target,通知所應用的物件
上例中,未寫出的 Service 所有方法都是連結點,被切入點表達式選中的方法都是切入點,而 AOP 類別的 recordTime 方法為通知,註解 @Around 與通知共同為切面,而 TimeAspect 類別稱為切面類別
通知
通知類型
@Around:環繞通知,此註解標註的通知方法在目標方法前、後都被執行
@Before:前置通知,此註解標註的通知方法在目標方法前被執行
@After:後置通知,此註解標註的通知方法在目標方法後被執行,無論是否有例外都會執行
@AfterReturning:返回後通知,此註解標註的通知方法在目標方法後被執行,有例外不會執行
@AfterThrowing:例外後通知,此註解標註的通知方法發生例外後執行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| @Component
@Aspect
public class MyAspect {
@Before("execution(* net.yexca.service.impl.*(..))")
public void before(){
System.out.println("Before");
}
@Around("execution(* net.yexca.service.impl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("Around before");
Object result = proceedingJoinPoint.proceed();
System.out.println("Around after");
return result;
}
@After("execution(* net.yexca.service.impl.*(..))")
public void after(){
System.out.println("After");
}
@AfterReturning("execution(* net.yexca.service.impl.*(..))")
public void afterRetruning(){
System.out.println("AfterReturning");
}
@AfterThrowing("execution(* net.yexca.service.impl.*(..))")
public void afterThrowing(){
System.out.println("AfterThrowing");
}
}
|
@Around 環繞通知需要自己呼叫 ProceedingJoinPoint.proceed() 來讓原始方法執行,其他通知不需要考量目標方法執行
@Around 環繞通知方法的回傳值,必須指定為 Object,來接收原始方法的回傳值
上述的 5 個註解的切入點表達式都相同,可以提取,如下所示
1
2
3
4
5
6
7
8
9
10
| public class MyAspect {
@Pointcut("execution(* net.yexca.service.impl.*(..))")
public void pt(){}
@Before("pt()")
public void before(){
System.out.println("Before");
}
}
|
方法 pt() 若是 public 權限符,則可以在其他的類別中引用
通知順序
當有多個切面的切入點都匹配到目標方法,目標方法執行時,多個通知方法都會被執行
1
2
3
4
5
6
| net:
yexca:
aop:
- MyAspect1
- MyAspect2
- MyAspect3
|
假設三個 AOP 類別都選中了同一個方法,不同切面類別中,預設是按照切面類別名稱字母排序
- 目標方法前的通知方法:字母排名靠前的先執行
- 目標方法後的通知方法:字母排名靠後的先執行
假設三個 AOP 類別都有 @Before 和 @After,執行順序為
1
2
3
4
5
6
| MyAspect1 before
MyAspect2 before
MyAspect3 before
MyAspect3 after
MyAspect2 after
MyAspect1 after
|
可以使用 @Order(num) 註解加在切面類別上來控制順序,num 越小越先執行,@Before 和 @After 執行同上
切入點表達式
描述切入點方法的一種表達式,主要用來決定專案中的哪些方法需要加入通知
常見形式有 execution(...) 根據方法的簽章匹配和 annotation 根據註解匹配
execution
主要根據方法的回傳值、套件名稱、類別名稱、方法名稱、方法參數等資訊來匹配,語法為
1
| execution(存取修飾符 回傳值 套件名稱.類別名稱.方法名稱(方法參數) throws 例外)
|
其中存取修飾符、套件名稱.類別名稱、throws 例外可以省略,不過不建議省略套件名稱.類別名稱
也可以使用萬用字元描述切入點
*:單個獨立的任意符號,可以萬用任意回傳值、套件名稱、類別名稱、方法名稱、任意類型的一個參數,也可以萬用套件、類別、方法名稱的一部分
1
| execution(* com.*.service.*.update*(*))
|
..:多個連續的任意符號,可以萬用任意層級的套件,或任意類型、任意個數的參數
1
| execution(* com.yexca..service.*(..))
|
還可以使用 &&、||、! 來組合比較複雜的切入點表達式
撰寫建議
- 所有業務方法名稱在命名時盡量規範,方便切入點表達式快速匹配
- 描述切入點方法通常基於介面描述,而非實作類別,增強擴充性
- 在滿足業務需求的前提下,盡量縮小切入點的匹配範圍
@annotation
@annotation 切入點表達式,用於匹配標示有特定註解的方法,使用需先自訂義註解
1
2
3
4
| @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}
|
然後在方法上加入該註解,在 AOP 類別方法上
1
| @Before("@annotation(net.yexca.aop.MyLog)")
|
連結點
在 Spring 中用 JoinPoint 抽象化了連結點,用它可以取得方法執行時的相關資訊
- 對於 @Around 通知,取得連結點資訊只能使用 ProceedingJoinPoint
- 對於其他四種通知,取得連結點資訊只能使用 JoinPoint,它是 ProceedingJoinPoint 的父類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 取得目標物件的類別名稱
String className = proceedingJoinPoint.getTarget().getClass().getName();
// 取得目標方法的方法名稱
String methodNAme = proceedingJoinPoint.getSignature().getName();
// 取得目標方法執行時傳入的參數
Object[] args = proceedingJoinPoint.getArgs();
// 呼叫原始方法
Object object = proceedingJoinPoint.proceed();
return object;
}
|