從XML配置角度理解Spring AOP

华为云开发者联盟發表於2024-05-13

本文分享自華為雲社群《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();
    }
}

執行結果:

從XML配置角度理解Spring AOP

透過結合動態代理技術和這些AOP概念,Spring AOP能夠以非侵入式的方式為應用程式提供橫切關注點的支援,這樣開發者就可以將這些關注點模組化,並保持業務邏輯元件的聚焦和簡潔。

如果對動態代理感興趣可以再除錯看看,這裡是JDK動態代理是因為public class MyServiceImpl implements MyService 實現了介面,除錯如下:

從XML配置角度理解Spring AOP

簡單說一下這裡能看到的關鍵類和介面

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>

除錯如下:

從XML配置角度理解Spring AOP

歡迎一鍵三連~

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

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

相關文章