一、AOP 是什么
AOP(Aspect Oriented Programming),即面向切面編程,是軟件開發中一種重要的編程范式。它通過橫向抽取機制,將那些與業務邏輯本身無關、卻為業務模塊所共同調用的邏輯或責任(如事務處理、日志管理、權限校驗等)封裝起來,然后通過“動態植入”的方式嵌入到業務邏輯的指定位置,從而實現業務邏輯的隔離與解耦。
AOP 是面向對象編程(OOP)的補充。在 OOP 中,我們通過類和繼承來組織代碼,但在某些情況下,會遇到一些問題。例如,當需要為多個不具有繼承關系的對象添加公共方法時,如日志記錄、性能監控等,如果采用 OOP 的方式,就需要在每個對象中都添加相同的方法,這會導致大量的重復代碼,增加維護成本。而 AOP 則可以很好地解決這個問題,它將這些公共邏輯抽取出來,集中管理,避免了重復代碼的產生。
二、AOP 的優勢
-
減少重復代碼:將公共邏輯集中到切面中,避免了在多個地方重復編寫相同的代碼。
-
提高開發效率:開發者可以專注于業務邏輯的實現,而無需在每個地方都處理那些公共的、與業務邏輯無關的邏輯。
-
方便維護:當需要修改公共邏輯時,只需修改切面中的代碼,而無需修改每個使用該邏輯的地方。
三、AOP 的技術要點
(一)通知(Advice)
通知定義了“什么時候”和“做什么”。它包含了需要用于多個應用對象的橫切行為。根據通知的執行時機,可以分為以下幾種類型:
-
前置通知(@Before):在目標方法調用之前調用通知。
-
后置通知(@After):在目標方法完成之后調用通知。
-
環繞通知(@Around):在被通知的方法調用之前和調用之后執行自定義的方法。需要注意的是,目標對象的方法需要手動執行。
-
返回通知(@AfterReturning):在目標方法成功執行之后調用通知。
-
異常通知(@AfterThrowing):在目標方法拋出異常之后調用通知。
(二)連接點(Join Point)
連接點是程序執行過程中能夠應用通知的所有點。在 Spring 中,連接點指的是方法,因為 Spring 只支持方法類型的連接點。
(三)切點(Pointcut)
切點定義了在“什么地方”進行切入,哪些連接點會得到通知。切點表達式用于明確指定方法的返回類型、類名、方法名和參數名等與方法相關的部件。常用的切點表達式格式為:execution([修飾符] 返回值類型 包名.類名.方法名(參數))
。其中,修飾符可以省略,返回值類型、包名、類名、方法名和參數都可以使用通配符(*
或 ..
)來表示。
(四)切面(Aspect)
切面是通知和切點的結合。通知和切點共同定義了切面的全部內容——是什么、何時、何地完成功能。
(五)引入(Introduction)
引入允許我們向現有的類中添加新方法或者屬性。
(六)織入(Weaving)
織入是把切面應用到目標對象并創建新的代理對象的過程。織入分為編譯期織入、類加載期織入和運行期織入。
四、AOP的底層原理
一、AOP 的底層原理概述
在 Spring 框架中,AOP 的實現依賴于動態代理技術。動態代理技術允許在運行時動態地創建代理對象,并在代理對象上調用方法時插入額外的邏輯(即通知)。Spring AOP 主要使用兩種動態代理技術:JDK 動態代理和 CGLIB 代理。
二、JDK 動態代理技術
JDK 動態代理是 Java 提供的一種標準代理機制,它依賴于 Java 的反射機制。JDK 動態代理的核心是 java.lang.reflect.Proxy
類和 InvocationHandler
接口。以下是 JDK 動態代理的實現步驟:
(一)為接口創建代理類的字節碼文件
-
定義接口:首先,需要定義一個接口,目標對象和代理對象都將實現這個接口。例如:
public interface UserService {void save(); }
-
實現目標類:目標類實現了上述接口,并提供了具體的業務邏輯。例如:
?public class UserServiceImpl implements UserService {@Overridepublic void save() {System.out.println("業務層:保存用戶...");} }
-
創建代理類:通過
java.lang.reflect.Proxy
類動態生成代理類。代理類實現了與目標類相同的接口,并在方法調用時插入額外的邏輯。例如:public class MyInvocationHandler implements InvocationHandler {private final Object target;public MyInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 在目標方法執行前插入邏輯System.out.println("前置通知:記錄日志");// 執行目標方法Object result = method.invoke(target, args);// 在目標方法執行后插入邏輯System.out.println("后置通知:記錄日志");return result;} }
-
生成代理實例:通過
Proxy.newProxyInstance
方法動態生成代理實例。例如:UserService userService = (UserService) Proxy.newProxyInstance(UserService.class.getClassLoader(), // 目標類的類加載器new Class<?>[]{UserService.class}, // 目標類實現的接口new MyInvocationHandler(new UserServiceImpl()) // 自定義的 InvocationHandler );
(二)使用 ClassLoader 將字節碼文件加載到 JVM
-
類加載器的作用:
ClassLoader
負責加載字節碼文件到 JVM 中。在 JDK 動態代理中,Proxy.newProxyInstance
方法會使用目標類的類加載器來加載生成的代理類。 -
動態生成字節碼:
Proxy
類會在運行時動態生成代理類的字節碼,并通過類加載器加載到 JVM 中。代理類的字節碼是基于目標類實現的接口動態生成的。
(三)創建代理類實例對象,執行對象的目標方法
-
代理類實例:通過
Proxy.newProxyInstance
方法生成的代理類實例對象,可以像普通對象一樣調用接口方法。 -
方法調用:當調用代理類實例的方法時,實際上會調用
InvocationHandler
的invoke
方法。在invoke
方法中,可以插入前置通知、后置通知等邏輯,并最終調用目標方法。
三、CGLIB 代理技術
CGLIB(Code Generation Library)是一個強大的字節碼生成庫,它可以在運行時動態生成目標類的子類,并覆蓋目標類的方法。CGLIB 代理主要用于那些沒有實現接口的類,或者需要代理類的方法而不是接口的方法。
(一)CGLIB 代理的基本原理
-
動態生成子類:CGLIB 通過字節碼操作庫(如 ASM)動態生成目標類的子類。生成的子類繼承了目標類,并覆蓋了目標類的方法。
-
方法攔截:在覆蓋的方法中,CGLIB 提供了一個
MethodInterceptor
接口,用于攔截方法調用。在攔截器中,可以插入額外的邏輯,并最終調用目標方法。
(二)CGLIB 代理的實現步驟
-
定義目標類:目標類不需要實現接口,例如:
public class UserServiceImpl {public void save() {System.out.println("業務層:保存用戶...");} }
-
創建攔截器:實現
?MethodInterceptor
接口,定義攔截邏輯。例如:public class MyMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {// 在目標方法執行前插入邏輯System.out.println("前置通知:記錄日志");// 執行目標方法Object result = proxy.invokeSuper(obj, args);// 在目標方法執行后插入邏輯System.out.println("后置通知:記錄日志");return result;} }
-
生成代理實例:通過
Enhancer
類動態生成代理實例。例如:Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserServiceImpl.class); // 設置目標類 enhancer.setCallback(new MyMethodInterceptor()); // 設置攔截器 UserServiceImpl userService = (UserServiceImpl) enhancer.create();
-
使用代理實例:通過代理實例調用方法時,實際上會調用攔截器的
intercept
方法,在其中插入額外的邏輯,并最終調用目標方法。
四、JDK 動態代理與 CGLIB 代理的比較
表格
特性 | JDK 動態代理 | CGLIB 代理 |
---|---|---|
適用場景 | 目標類必須實現接口 | 目標類可以沒有接口 |
代理方式 | 通過接口代理 | 通過生成子類代理 |
性能 | 相對較好,因為基于接口調用 | 稍差,因為需要生成子類并覆蓋方法 |
靈活性 | 只能代理接口方法 | 可以代理類的方法,更靈活 |
實現機制 | 基于 Java 反射和?InvocationHandler | 基于字節碼操作庫(如 ASM) |