[源碼系列:手寫spring] AOP第一節(jié):切點表達式
在本專欄之前的文章中已經(jīng)帶大家熟悉了Spirng中核心概念IOC的原理以及手寫了核心代碼,接下來將繼續(xù)介紹Spring中另一核心概念AOP。
AOP即切面編程是Spring框架中的一個關鍵概念,它允許開發(fā)者在應用程序中優(yōu)雅地處理橫切關注點,如日志記錄、性能監(jiān)控和事務管理。在切面編程中,切點表達式是一項關鍵技術,它定義了在何處應用切面的邏輯。本章將深入探討Spring切點表達式的實現(xiàn)原理,為讀者提供對這一重要概念的深刻理解。
1.AOP案例
1.1 案例背景
假設我們有一個在線商城的Web應用,用戶可以瀏覽商品、下單購買商品等。我們希望記錄每個HTTP請求的開始時間、結束時間以及執(zhí)行時間,以便監(jiān)控應用的性能并快速定位潛在的問題。
1.2 AOP解決方案
我們可以使用Spring AOP和切點表達式來實現(xiàn)這個日志記錄功能。以下是實現(xiàn)步驟:
1. 創(chuàng)建一個切面類:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RequestLoggingAspect {
private long startTime;
@Before("execution(* com.example.controller.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
startTime = System.currentTimeMillis();
System.out.println("Request received for: " + joinPoint.getSignature().toShortString());
}
@After("execution(* com.example.controller.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
System.out.println("Request completed for: " + joinPoint.getSignature().toShortString());
System.out.println("Execution Time: " + executionTime + "ms");
}
}
上述代碼定義了一個切面類 RequestLoggingAspect,其中包含兩個通知方法 logBefore 和 logAfter。logBefore 方法在方法執(zhí)行前記錄開始時間和請求信息,而 logAfter 方法在方法執(zhí)行后記錄結束時間和執(zhí)行時間。
2. 配置切點表達式:
在切面類中,我們使用 @Before 和 @After 注解分別標注了 logBefore 和 logAfter 方法,并指定了切點表達式 "execution(* com.example.controller.*.*(..))"。這個切點表達式表示我們希望攔截所有 com.example.controller 包下的方法執(zhí)行。
3. 啟用AspectJ支持:
確保在Spring配置文件中啟用了AspectJ的支持。可以通過以下配置實現(xiàn):
<aop:aspectj-autoproxy />
在Spring Boot應用的配置類中,保證@Aspect標記的切面類正確被容器管理即可。
4. 結果:
當用戶發(fā)起HTTP請求時,切面會自動攔截匹配的方法,記錄請求的開始時間和結束時間,并輸出到日志中。這樣,我們就能夠實時監(jiān)控每個請求的性能,并在需要時進行故障排除。
2. 知識補充
2.1 切點和連接點的概念
在Spring框架中,切點(Pointcut)和連接點(JoinPoint)是實現(xiàn)切面編程的兩個核心概念。切點定義了在應用程序中哪些地方切入(或觸發(fā))切面的邏輯,而連接點則代表在應用程序執(zhí)行過程中的具體執(zhí)行點。連接點可以是方法的調(diào)用、方法的執(zhí)行、異常的拋出等。理解這兩個概念是理解切點表達式的基礎。
- 連接點(JoinPoint)是程序執(zhí)行的特定點,它可以是方法的執(zhí)行、方法的調(diào)用、對象的創(chuàng)建等。在Spring AOP中,連接點通常表示方法的執(zhí)行。連接點是AOP切面可以插入的地方,例如,我們可以在方法調(diào)用之前或之后插入額外的邏輯。
- 切點(Pointcut)是一個表達式,它定義了連接點的集合。換句話說,切點確定了在哪些連接點上切入切面邏輯。Spring框架支持多種切點表達式的定義,其中最常用的是AspectJ切點表達式。
2.1.1 AspectJ切點表達式
AspectJ是一種強大的面向切面編程(AOP)語言,Spring框架引入了AspectJ切點表達式以方便開發(fā)者定義切點。AspectJ切點表達式使用一種類似于正則表達式的語法來匹配連接點。
AspectJ切點表達式的語法包括以下幾個關鍵部分:
- execution關鍵字:用于指定要匹配的方法執(zhí)行連接點。
//匹配com.example.service包中的所有類的所有方法
execution(* com.example.service.*.*(..))
- 訪問修飾符和返回類型:可以使用通配符來匹配任意修飾符或返回類型。
//匹配任何公共方法的執(zhí)行。
execution(public * com.example.service.*.*(..))
- 包和類的限定符:用于指定包和類的名稱,通配符`*`可用于匹配任意字符。
//匹配com.example.service包中UserService類的所有方法執(zhí)行。
execution(* com.example.service.UserService.*(..))
- 方法名:可以指定具體的方法名或使用通配符匹配多個方法。
//匹配UserService類中以"get"開頭的所有方法執(zhí)行。
execution(* com.example.service.UserService.get*(..))
- 參數(shù)列表:可以使用“ (..) ”來匹配任意參數(shù)列表。
//匹配UserService類的所有方法執(zhí)行,無論參數(shù)列表如何
execution(* com.example.service.UserService.*(..))
AspectJ切點表達式的靈活性使開發(fā)者能夠定義精確的切點,以滿足不同的應用需求。通過深入學習和掌握AspectJ切點表達式,開發(fā)者可以更好地利用Spring AOP來管理應用程序中的橫切關注點。接下來,我們將深入研究切點表達式的實現(xiàn)原理,以更好地理解Spring框架是如何解析和匹配這些表達式的。
3. 實現(xiàn)原理
3.1代碼分支
3.2 核心代碼
ClassFilter 和 MethodMatcher 接口
- ClassFilter:該接口用于篩選出應該應用切面的目標類。在Pointcut表達式中,如果沒有指定特定的目標類,ClassFilter將返回true,表示匹配任何類。否則,它將根據(jù)指定的規(guī)則篩選出匹配的類。
- MethodMatcher:這個接口用于匹配目標類中的方法。MethodMatcher決定了哪些方法會成為連接點,從而被切面攔截。MethodMatcher接口包括兩個方法:`matches(Method method, Class<?> targetClass)` 用于匹配方法,和 `isRuntime()` 用于表示匹配是否需要在運行時進行動態(tài)計算。
AspectJExpressionPointcut 的簡單實現(xiàn)
- 表達式解析:首先,AspectJExpressionPointcut會將切點表達式進行解析,將其轉化為內(nèi)部的數(shù)據(jù)結構,以便進行進一步處理。這個解析過程涉及到詞法分析和語法分析,以確保切點表達式的語法正確性。
- 連接點匹配:一旦切點表達式被解析,AspectJExpressionPointcut 將會使用 ClassFilter 和 MethodMatcher 接口來匹配連接點。它會遍歷應用程序中的類和方法,根據(jù)表達式的定義,確定哪些連接點符合切點表達式的要求。
- 運行時動態(tài)匹配:在某些情況下,切點表達式可能需要在運行時動態(tài)計算。例如,當表達式中包含參數(shù)綁定時,需要在實際方法執(zhí)行時才能確定是否匹配。AspectJExpressionPointcut會在運行時進行動態(tài)匹配,以確保準確的連接點匹配。
下面將借助aspectjweaver的功能簡單實現(xiàn)Spring AOP切點表達式功能,實現(xiàn)對execution函數(shù)的支持。
3.2.1 首先添加maven坐標
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.0</version>
</dependency>
| 特征 | Spring AOP | AspectJ |
|---|---|---|
| 編程模型 | 基于代理的編程模型,使用 Spring 代理生成 AOP 代理。 | 純粹基于注解或 XML 的編程模型,使用 AspectJ 編譯器或運行時織入器。 |
| 編織方式 | 運行時織入,通過代理包裝目標對象來添加切面行為。 | 支持編譯時織入和運行時織入,更靈活且功能更強大。 |
| 性能 | 由于使用代理,性能開銷較小,但有些限制。 | 性能較好,編譯時織入可以最小化運行時開銷。 |
| 支持的切入點表達式(Pointcut) | 僅支持一部分切入點表達式,如方法執(zhí)行(execution)。 | 支持廣泛的切入點表達式,包括訪問、調(diào)用、初始化等多種方式。 |
| 復雜度 | 適用于簡單的切面需求,易于配置和使用。 | 適用于復雜的切面需求,提供更多高級功能和靈活性。 |
| 集成度 | 緊密集成到 Spring 框架中,易于使用和配置。 | 相對獨立,需要額外配置 AspectJ 編譯器或運行時織入器。 |
| 配置方式 | 使用 Spring 的注解或 XML 配置來定義切面。 | 使用 AspectJ 注解或 XML 配置來定義切面。 |
3.2.2 ClassFilter接口
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
3.2.3 MethodMatcher接口
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
}
3.2.4 Pointcut 切點接口
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
3.2.5 AspectJExpressionPointcut 切點表達式類
/**
* ● @author: YiHui
* ● @date: Created in 17:33 2023/9/24
* ● @Description: 這是一個自定義的 AspectJ 表達式切點,用于在 Spring AOP 中匹配切點表達式。
*/
public class AspectJExpressionPointcut implements Pointcut, ClassFilter, MethodMatcher {
// 支持的切點原語集合
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();
static {
// 添加支持的切點原語。在此示例中,我們僅支持 EXECUTION 原語,您可以根據(jù)需要添加更多。
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
}
// 切點表達式對象,用于解析和匹配切點
private final PointcutExpression pointcutExpression;
/**
* 構造函數(shù),用給定的表達式創(chuàng)建 AspectJ 表達式切點。
*
* @param expression 切點表達式,用于定義匹配的切點
*/
public AspectJExpressionPointcut(String expression) {
// 創(chuàng)建一個 PointcutParser 實例,用于解析切點表達式
PointcutParser pointcutParser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(
SUPPORTED_PRIMITIVES, this.getClass().getClassLoader());
// 解析給定的切點表達式并將其分配給成員變量 pointcutExpression
pointcutExpression = pointcutParser.parsePointcutExpression(expression);
}
/**
* 檢查給定的類是否符合切點表達式的條件。
*
* @param clazz 要檢查的類
* @return 如果類匹配切點表達式,則返回 true,否則返回 false
*/
@Override
public boolean matches(Class<?> clazz) {
return pointcutExpression.couldMatchJoinPointsInType(clazz);
}
/**
* 檢查給定的方法是否符合切點表達式的條件。
*
* @param method 要檢查的方法
* @param targetClass 方法所屬的目標類
* @return 如果方法匹配切點表達式,則返回 true,否則返回 false
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
// 使用切點表達式檢查方法執(zhí)行是否匹配
return pointcutExpression.matchesMethodExecution(method).alwaysMatches();
}
/**
* 獲取用于類篩選的 ClassFilter 實例。
*
* @return ClassFilter 實例,用于過濾匹配的類
*/
@Override
public ClassFilter getClassFilter() {
return this;
}
/**
* 獲取用于方法匹配的 MethodMatcher 實例。
*
* @return MethodMatcher 實例,用于匹配符合切點表達式的方法
*/
@Override
public MethodMatcher getMethodMatcher() {
return this;
}
}
4. 測試
4.1測試代碼
public class HelloService {
public String hello() {
System.out.println("hello word! yihuiComeOn");
return "hello word! yihuiComeOn";
}
}
public class PointcutExpressionTest {
@Test
public void testPointcutExpression() throws Exception {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut("execution(* service.HelloService.*(..))");
Class<HelloService> clazz = HelloService.class;
Method method = clazz.getDeclaredMethod("hello");
System.out.println("切點表達式匹配結果-類匹配:"+pointcut.matches(clazz));
System.out.println("切點表達式匹配結果-方法匹配:"+pointcut.matches(method, clazz));
}
}
4.2 測試結果
切點表達式匹配結果-類匹配:true
切點表達式匹配結果-方法匹配:true

https://github.com/yihuiaa/little-spring/tree/pointcut-expression
浙公網(wǎng)安備 33010602011771號