本文分享自華爲雲社區《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
限定這些位置爲方法的調用。簡而言之,連接點就是能夠插入切面通知的點。
通知(Advice):通知定義了切面在連接點上要執行的動作。根據通知類型的不同,這些動作可以在方法調用之前、之後、返回結果後或拋出異常時執行。通知類型包括:
- 前置通知(
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>
調試如下:
歡迎一鍵三連~
有問題請留言,大家一起探討學習