本文分享自華為雲社群《Spring高手之路18——從XML配置角度理解Spring AOP》,作者: 磚業洋__。
1. Spring AOP與動態代理
1.1 Spring AOP和動態代理的關係
Spring AOP
使用動態代理作為其主要機制來實現面向切面的程式設計。這種機制允許Spring
在執行時動態地建立代理物件,這些代理物件包裝了目標物件(即業務元件),以便在呼叫目標物件的方法前後插入額外的行為(如安全檢查、事務管理、日誌記錄等)。
-
JDK動態代理:當目標物件實現了一個或多個介面時,
Spring AOP
預設使用JDK
的動態代理。JDK
動態代理透過反射機制,為介面建立一個代理物件,這個代理物件會攔截對目標介面方法的所有呼叫。 -
CGLIB代理:如果目標物件沒有實現任何介面,
Spring AOP
會退回到使用CGLIB
庫生成目標類的子類。CGLIB
(Code Generation Library
)是一個強大的高效能程式碼生成庫,它在執行時擴充套件了Java
類,並在子類中覆蓋了方法來實現方法攔截。
無論使用哪種代理方式,目的都是在不改變原有業務邏輯程式碼的基礎上,透過切面定義的通知在方法執行的不同階段插入附加行為。
1.2 AOP基本術語
切面(Aspect):切面是面向切面程式設計的核心,它是將橫跨多個類的關注點(如日誌記錄、事務管理等)模組化的構造。一個切面可以包含多種型別的通知(Advice
)和一個或多個切點(Pointcut
),用於定義在何處以及何時執行這些通知。
連線點(Join Point):連線點代表程式執行過程中的某個特定位置,Spring AOP
限定這些位置為方法的呼叫。簡而言之,連線點就是能夠插入切面通知的點。
- 前置通知(
Before advice
):在方法執行之前執行。 - 後置通知(
After advice
):在方法執行後執行,無論其結果如何。 - 返回後通知(
After-returning advice
):在方法成功執行之後執行。 - 異常後通知(
After-throwing advice
):在方法丟擲異常後執行。 - 環繞通知(
Around advice
):在方法執行之前和之後執行,提供對方法呼叫的全面控制。
切點(Pointcut):切點是一個表示式,切點表示式允許透過方法名稱、訪問修飾符等條件來匹配連線點,決定了通知應該在哪些方法執行時觸發。
目標物件(Target Object):被一個或多個切面所通知的物件。也被稱為被代理物件。
AOP代理(AOP Proxy):AOP
框架建立的物件,用於實現切面契約(由通知和切點定義)。在Spring AOP
中,AOP
代理可以是JDK
動態代理或CGLIB
代理。
引入(Introduction):引入允許向現有的類新增新的方法或屬性。這是透過定義一個或多個附加介面(Introduction interfaces
)實現的,AOP
框架會為目標物件建立一個代理,該代理實現這些介面。
如果還是覺得抽象,我們再舉一個電影製作的例子來類比
切面(Aspect)
想象一下,有人正在拍攝一部電影,而電影中的特效(比如爆炸和特殊光效)就像是應用程式中需要處理的橫切關注點(比如日誌記錄或事務管理)。這些特效會在電影的許多不同場景中出現,而不僅僅侷限於某一個特定場景。在AOP
中,這些“特效”就是切面,它們可以被應用到程式的多個部分,而不需要改變實際的場景(或程式碼)。
連線點(Join Point)
繼續使用電影的比喻,每個場景中的特定時刻,比如一個爆炸發生的瞬間,可以看作是一個連線點。在程式設計中,這通常對應於方法的呼叫。
通知(Advice)
通知就像是導演對特效團隊的具體指令,比如“在這個場景開始之前加入一個爆炸效果”或“場景結束後顯示煙霧漸散的效果”。這些指令告訴特效團隊在電影的哪個具體時刻應該新增特定的效果。在AOP
中,這些“指令”就是通知,指定了切面(特效)應該在連線點(特定的程式碼執行時刻)之前、之後或周圍執行。
切點(Pointcut)
如果說通知是導演對特效團隊的指令,那麼切點就是指令中包含的具體條件,比如“所有夜晚的外景戲”。切點定義了哪些連線點(比如哪些具體的方法呼叫)應該接收通知(特效指令)。
目標物件(Target Object)
目標物件就是那些需要新增特效的場景。在我們的程式設計比喻中,它們是那些被切面邏輯影響的物件(比如需要日誌記錄的類)。
AOP代理(AOP Proxy)
AOP
代理就像是特效團隊提供的一個虛擬的、可控制特效的場景副本。這個副本在觀眾看來與原場景無異,但實際上它能在導演需要的時刻自動新增特效。在程式設計中,代理是一個被AOP
框架自動建立的物件,它包裝了目標物件,確保了通知(特效指令)在正確的時間被執行。
引入(Introduction)
引入就好比是在電影中加入一個全新的角色或者場景,這在原本的指令碼中並不存在。在AOP
中,引入允許我們向現有的類新增新的方法或屬性,這就像是在不改變原始指令碼的情況下擴充套件電影的內容。
2. 透過XML配置實現Spring AOP
Spring
提供了豐富的AOP
支援,可以透過XML
配置來定義切面、通知(advice
)和切點(pointcuts
)。這樣可以在不修改原始碼的情況下增加額外的行為(如日誌、事務管理等)
實現步驟:
-
新增Spring依賴:在專案的
pom.xml
中新增Spring
框架和AOP
相關的依賴。 -
定義業務介面和實現類:建立業務邏輯介面及其實現,比如一個簡單的服務類。
-
定義切面類:建立一個切面類,用於定義前置、後置、環繞等通知。
-
配置XML:在
applicationContext.xml
中配置切面和業務bean
,以及AOP
相關的標籤。
2.1 新增Spring依賴
在pom.xml
檔案中,新增以下依賴
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.10</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.3.10</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency> </dependencies>
2.2 定義業務介面和實現類
首先,我們定義一個業務邏輯介面MyService
和它的實現MyServiceImpl
。
MyService.java:
package com.example.demo.aop; public interface MyService { String performAction(String input) throws Exception; }
MyServiceImpl.java:
package com.example.demo.aop; public class MyServiceImpl implements MyService { @Override public String performAction(String action) throws Exception { System.out.println("Performing action in MyService: " + action); if ("throw".equals(action)) { throw new Exception("Exception from MyService"); } return "Action performed: " + action; } }
2.3 定義切面類
接下來,我們定義一個切面類MyAspect
,這個類將包含一個前置通知(advice
),它在MyService
的performAction
方法執行之前執行。
MyAspect.java:
package com.example.demo.aop; import org.aspectj.lang.ProceedingJoinPoint; public class MyAspect { // 前置通知 public void beforeAdvice() { System.out.println("Before advice is running!"); } // 後置通知 public void afterAdvice() { System.out.println("After advice is running!"); } // 返回後通知 public void afterReturningAdvice(Object retVal) { System.out.println("After returning advice is running! Return value: " + retVal); } // 異常後通知 public void afterThrowingAdvice(Throwable ex) { System.out.println("After throwing advice is running! Exception: " + ex.getMessage()); } // 環繞通知 public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("Around advice: Before method execution"); Object result = null; try { result = joinPoint.proceed(); } finally { System.out.println("Around advice: After method execution"); } return result; } }
2.4 配置XML
最後,我們需要在Spring
的配置檔案applicationContext.xml
中配置上述bean
以及AOP
的相關內容。
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 上面這是XML檔案的頭部宣告,它定義了檔案的版本和編碼型別,同時引入了Spring beans 和 AOP 的名稱空間。 透過這些名稱空間,我們可以在XML中使用<bean>和<aop:*>標籤。 --> <!-- Bean definitions --> <bean id="myService" class="com.example.demo.aop.MyServiceImpl"/> <bean id="myAspect" class="com.example.demo.aop.MyAspect"/> <!-- AOP配置 --> <aop:config> <!-- 定義切面及其通知 --> <aop:aspect id="myAspectRef" ref="myAspect"> <!-- 定義切點,指定通知應該在哪些方法執行時觸發 --> <aop:pointcut id="serviceOperation" expression="execution(* com.example.demo.aop.MyService.performAction(..))"/> <!-- 應用前置通知,指定方法執行前的操作 --> <aop:before method="beforeAdvice" pointcut-ref="serviceOperation"/> <!-- 應用後置通知,指定方法執行後的操作,不論方法執行成功還是丟擲異常 --> <aop:after method="afterAdvice" pointcut-ref="serviceOperation"/> <!-- 應用返回後通知,指定方法成功執行並返回後的操作 --> <aop:after-returning method="afterReturningAdvice" pointcut-ref="serviceOperation" returning="retVal"/> <!-- 應用異常後通知,指定方法丟擲異常後的操作 --> <aop:after-throwing method="afterThrowingAdvice" pointcut-ref="serviceOperation" throwing="ex"/> <!-- 應用環繞通知,提供方法執行前後的完全控制 --> <aop:around method="aroundAdvice" pointcut-ref="serviceOperation"/> </aop:aspect> </aop:config> </beans>
myService:這是業務邏輯的bean
,指向MyServiceImpl
類的例項。
myAspect:這是切面的bean
,指向MyAspect
類的例項。
<aop:config>
:這是AOP
配置的根元素,所有的AOP
配置,包括切面定義、切點和通知方法等,都需要在此元素內部定義。
切面(Aspect):透過<aop:aspect>
元素定義,它包含了一系列通知(advice
)和一個或多個切點(pointcut
)。這個元素將切面類(包含通知邏輯的類)與具體的操作(如何、何時對目標物件進行增強)關聯起來。
切點(Pointcut):透過<aop:pointcut>
元素定義,切點透過表示式來指定,當需要精確控制哪些方法執行時會觸發通知時,就需要定義切點。切點表示式可以非常精確地指定方法,例如透過方法名稱、引數型別、註解等。expression
定義了切點的表示式,指明瞭切點的匹配規則。這裡的表示式execution(* com.example.demo.aop.MyService.performAction(..))
意味著切點匹配MyService
介面中performAction
方法的執行,切點用於指定在哪些連線點(Join Point
,例如方法呼叫)上應用通知。
關於解析表示式execution(* com.example.demo.aop.MyService.performAction(..))
execution:是最常用的切點函式,用於匹配方法執行的連線點。
*:表示方法的返回型別是任意的。
com.example.demo.aop.MyService.performAction:指定了全路徑的介面名和方法名。
(…):表示方法引數是任意的,無論方法有多少個引數都匹配。
-
連線點(Join Point):連線點是指在程式執行過程中的某一點,比如方法的呼叫。 連線點是透過切點(
Pointcut
)的表示式來識別和匹配的,execution(* com.example.demo.aop.MyService.performAction(..))
表示式定義了一個切點,它指定了一個明確的連線點集合——即MyService
介面的performAction
方法的所有呼叫。這個例子中,MyService
介面的performAction
方法的呼叫就是潛在的連線點。每次performAction
方法被呼叫時,就達到了一個連線點。這個連線點就是這裡通知應用的時機。 -
通知(Advice):這是
AOP
透過在特定時機執行的操作來增強方法的執行。method
屬性指明當切點匹配時應該執行的切面的方法名,pointcut-ref
引用了上面定義的切點。比如這裡的beforeAdvice
是在目標方法performAction
執行之前被呼叫的方法。這意味著每當MyService.performAction(..)
方法被呼叫時,beforeAdvice
方法將首先被執行。
總結為一句話:Spring AOP
透過在切面中定義規則(切點)來指定何時(連線點)以及如何(通知)增強特定方法,實現程式碼的模組化和關注點分離,無需修改原有業務邏輯。
透過這種方式,Spring AOP
允許定義在特定方法執行前、執行後、環繞執行等時機插入自定義邏輯,而無需修改原有業務邏輯程式碼。這是實現關注點分離的一種強大機制,特別是對於跨越應用程式多個部分的橫切關注點(如日誌、事務管理等)。
注意,如果<aop:config>
設定為
<aop:config proxy-target-class="true"> <!-- 其他配置不變 --> </aop:config>
設定proxy-target-class="true"
會使Spring AOP
優先使用CGLIB
代理,即使目標物件實現了介面。預設情況下,不需要設定proxy-target-class
屬性,或者將其設定為false
,則是使用JDK
動態代理。
主程式:
DemoApplication.java:
package com.example.demo; import com.example.demo.aop.MyService; import org.springframework.context.support.ClassPathXmlApplicationContext; public class DemoApplication { public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); MyService myService = (MyService) context.getBean("myService"); try { System.out.println(myService.performAction("normal")); } catch (Exception e) { e.printStackTrace(); } System.out.println("======================="); try { System.out.println(myService.performAction("throw")); } catch (Exception e) { System.out.println("Exception caught in main: " + e.getMessage()); } context.close(); } }
執行結果:
透過結合動態代理技術和這些AOP
概念,Spring AOP
能夠以非侵入式的方式為應用程式提供橫切關注點的支援,這樣開發者就可以將這些關注點模組化,並保持業務邏輯元件的聚焦和簡潔。
如果對動態代理感興趣可以再除錯看看,這裡是JDK
動態代理是因為public class MyServiceImpl implements MyService
實現了介面,除錯如下:
簡單說一下這裡能看到的關鍵類和介面
ProxyFactory: 這是Spring AOP
用來建立代理物件的工廠類。它可以根據目標物件是否實現介面來決定使用JDK
動態代理還是CGLIB
代理。
AopProxy: 這個介面定義了獲取代理物件的方法。它有兩個主要實現:JdkDynamicAopProxy
(用於JDK
動態代理)和CglibAopProxy
(用於CGLIB
代理)。
JdkDynamicAopProxy: 實現了AopProxy
介面,使用JDK
動態代理技術建立代理。它實現了InvocationHandler
介面,攔截對代理物件的所有方法呼叫。
CglibAopProxy: 同樣實現了AopProxy
介面,但使用CGLIB
庫來建立代理物件。對於沒有實現介面的類,Spring
會選擇這種方式來建立代理。
如果大家想深入瞭解Spring AOP
的原始碼,可以直接檢視JdkDynamicAopProxy
和CglibAopProxy
這兩個類的實現。這裡不是本篇重點,簡單提一下:
比如在JdkDynamicAopProxy
中看到動態代理的實現:
-
JdkDynamicAopProxy
類實現了InvocationHandler
介面,這是JDK
動態代理的核心。在其invoke
方法中,會有邏輯判斷是否需要對呼叫進行攔截,並在呼叫前後應用相應的通知。 -
建立代理的過程主要是在
ProxyFactory
透過呼叫createAopProxy()
方法時完成的,這個方法會根據配置返回JdkDynamicAopProxy
或CglibAopProxy
的例項。 -
代理的使用:客戶端程式碼透過
ProxyFactory
獲取代理物件,並透過這個代理物件呼叫目標方法。代理物件在內部使用JdkDynamicAopProxy
或CglibAopProxy
來攔截這些呼叫,並根據AOP
配置執行通知。透過ProxyFactory
獲取代理物件的過程,通常在Spring
的配置和使用中是隱式完成的,特別是在使用Spring
容器管理AOP
時。這一過程不需要開發者直接呼叫ProxyFactory
類。當Spring
配置中定義了一個bean
,並對其應用了切面,Spring
容器會自動處理代理的建立和應用通知的過程。這是透過Spring
的後處理器和AOP
名稱空間的支援實現的,開發者通常只需宣告式地配置切面和通知即可。
如果想看到CGLIB
代理,這裡有2
種方法
第1
種方法是去掉MyServiceImpl
實現的MyService
介面,然後把主程式和expression
表示式對應的地方改成MyServiceImpl
。
第2
種方法就是Spring
配置檔案中顯式設定<aop:config>
標籤的proxy-target-class="true"
屬性來實現這一點。如下:
<aop:config proxy-target-class="true"> <!-- 其他配置保持不變 --> </aop:config>
除錯如下:
歡迎一鍵三連~
有問題請留言,大家一起探討學習
點選關注,第一時間瞭解華為雲新鮮技術~