從XML配置角度理解Spring AOP

本文分享自華爲雲社區《Spring高手之路18——從XML配置角度理解Spring AOP》,作者: 磚業洋__。

1. Spring AOP與動態代理

1.1 Spring AOP和動態代理的關係

Spring AOP使用動態代理作爲其主要機制來實現面向切面的編程。這種機制允許Spring在運行時動態地創建代理對象,這些代理對象包裝了目標對象(即業務組件),以便在調用目標對象的方法前後插入額外的行爲(如安全檢查、事務管理、日誌記錄等)。

  • JDK動態代理:當目標對象實現了一個或多個接口時,Spring AOP默認使用JDK的動態代理。JDK動態代理通過反射機制,爲接口創建一個代理對象,這個代理對象會攔截對目標接口方法的所有調用。

  • CGLIB代理:如果目標對象沒有實現任何接口,Spring AOP會退回到使用CGLIB庫生成目標類的子類。CGLIBCode 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)。這樣可以在不修改源代碼的情況下增加額外的行爲(如日誌、事務管理等)

實現步驟:

  1. 添加Spring依賴:在項目的pom.xml中添加Spring框架和AOP相關的依賴。

  2. 定義業務接口和實現類:創建業務邏輯接口及其實現,比如一個簡單的服務類。

  3. 定義切面類:創建一個切面類,用於定義前置、後置、環繞等通知。

  4. 配置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),它在MyServiceperformAction方法執行之前執行。

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的源碼,可以直接查看JdkDynamicAopProxyCglibAopProxy這兩個類的實現。這裏不是本篇重點,簡單提一下:

比如在JdkDynamicAopProxy中看到動態代理的實現:

  1. JdkDynamicAopProxy類實現了InvocationHandler接口,這是JDK動態代理的核心。在其invoke方法中,會有邏輯判斷是否需要對調用進行攔截,並在調用前後應用相應的通知。

  2. 創建代理的過程主要是在ProxyFactory通過調用createAopProxy()方法時完成的,這個方法會根據配置返回JdkDynamicAopProxyCglibAopProxy的實例。

  3. 代理的使用:客戶端代碼通過ProxyFactory獲取代理對象,並通過這個代理對象調用目標方法。代理對象在內部使用JdkDynamicAopProxyCglibAopProxy來攔截這些調用,並根據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>

調試如下:

 

歡迎一鍵三連~

有問題請留言,大家一起探討學習

點擊關注,第一時間瞭解華爲雲新鮮技術~

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章