Spring 面向切面程式設計AOP 詳細講解

Rainbow-Sea發表於2024-05-18

1. Spring 面向切面程式設計AOP 詳細講解

@

目錄
  • 1. Spring 面向切面程式設計AOP 詳細講解
  • 每博一文案
  • 2. AOP介紹說明
    • 2.1 AOP的七大術語
    • 2.2 AOP 當中的 切點表示式
  • 3. 使用Spring 對 AOP 的實現使用
    • 3.1 準備工作
    • 3.2 Spring 基於AspectJ的AOP註解式開發
      • 3.2.1 實現步驟
      • 3.2.2 各個通知型別的說明
      • 3.2.3 切面的先後順序的設定
      • 3.2.4 最佳化切點表示式的方式
      • 3.2.5 Spring 全註解式開發ACP
    • 3.3 Spring 基於XML配置方式的AOP(瞭解)
    • 3.4 AOP 的實際案例:事務處理
    • 3.5 AOP 的實際案例:安全日誌
  • 4. 總結:
  • 5. 最後:


每博一文案

你逢人就炫耀的玫瑰,枯萎時該如何收場
我炫耀過的玫瑰永遠都不會枯萎
可能有一天它選擇了更好的土壤
但仍然會在我這裡留下芳香
當我炫耀它的時候
就沒想過束縛它
它開的更美,更豔才是我的初衷


Spring IOC 是軟體元件松耦合度,而AOP讓你能夠捕捉系統中經常使用的功能,把它轉化為元件。

AOP(Aspect Oriented Programming):面向切面程式設計面向方面程式設計 。(AOP是一種程式設計技術)

AOP 是對OOP的補充延申。

AOP底層使用的就是動態代理,關於動態代理,想要了解更多的,大家可以移步至:✏️✏️✏️ GoF之代理模式(靜態代理+動態代理(JDK動態代理+CGLIB動態代理帶有一步一步詳細步驟))-CSDN部落格

Spring 的AOP使用的是動態代理是: JDK動態代理 + CGLIB 動態代理技術 。Spring 在這兩種動態代理中靈活切換,如果是代理介面,會預設使用JDK動態代理,如果要代理某個類,這個類沒有實現介面,就會切換使用CGLIB。當然,你也可以強制透過一些配置讓Spring 只使用 CGLIB。(讓文章內容有所說明)

2. AOP介紹說明

一般一個系統當中都會有一些系統服務,例如:日誌,事務處理,安全等,這些系統服務被稱為:交叉業務 。這些交叉業務 幾乎是通用的,不管你是做銀行賬戶轉賬,還是刪除使用者資料。日誌,事務處理,安全,這些都是需要做的。

如果在每一個業務處理過程當中,都參雜這些交叉業務 程式碼進去的話,存在兩方面的問題:

  1. 第一:交叉業務程式碼在多個業務流程中反覆出現,顯然這個交叉業務 程式碼並沒有得到充分的複用,並且修改這些交叉業務 程式碼的話,需要修改多處。
  2. 第二:程式設計師無法專注核心業務程式碼的編寫,在編寫核心業務程式碼的同時還需要處理這些交叉業務。

使用 AOP 可以很輕鬆的解決以上問題。

如下圖:可以更好的理解 AOP思想。

在這裡插入圖片描述

簡單的說AOP:就是將與核心業務 無關的程式碼抽離開來,形成一個獨立的元件,然後,以橫向 交叉的方式應用到業務流程當中的過程被稱為 AOP

AOP的優點:

  1. 程式碼的複用性增強
  2. 程式碼易維護
  3. 使開發者更關注業務邏輯

2.1 AOP的七大術語

  • 1 連線點 JoinPoint

在程式的整個執行流程中,可以切入 的位置,方法的執行前後,異常丟擲之後等位置。

  • 2 切點 Pointcut

在程式執行流程中,真正織入 切面的方法。(一個切點對應多個連線點)

  • 3 通知 Advice

通知叫增強,就是具體你要插入\新增 的程式碼。

通知包括:

  1. 前置通知
  2. 後置通知
  3. 環繞通知
  4. 異常通知
  5. 最終通知
  • 4 切面 Aspect切面 = 切點 + 通知*
  • 5 織入 Weaving

把通知應用到目標物件的過程

  • 6 代理物件 Proxy

一個目標物件被織入通知後產生的新物件

  • 7 目標物件 Target

被織入通知的物件。

在這裡插入圖片描述

2.2 AOP 當中的 切點表示式

所謂的切點表示式:就是用來定義通知(Advice) 往哪些方法上切入的。

切入點表示式語法表達格式:

execution([訪問控制許可權修飾符] 返回值型別 [全限定類名]方法名(形式引數列表) [異常])
  1. 訪問控制許可權修飾符:

    可選項,可以沒有
    沒寫,就是預設包括了4個許可權
    寫public 就表示只包括公開的方法。
                 
    
  1. 返回值型別:
必須要有
“*” 表示返回值型別任意
  1. 全限定類名
可選項:可以不寫
“..” 兩個點,表示當前包以及子包下的所有類
省略:表示包括所有的類;就是所有
  1. 方法名
必填項
"*" 表示該對應包下的所有任意方法() 就是所有的方法
set* 則表示所有 set 開頭的方法()
get* 則表示所有 get 開頭的方法()
  1. 形式引數列表
必填項
() 空括號,表示沒有引數的方法。
(..) 括號中有兩個點,表示引數型別和引數個數任意的方法,
(*) 表示只有一個引數的方法
(*,String) 第一個引數型別隨意,第二個引數型別必須是String 型別才行
  1. 異常:
可選項
省略表示任意異常型別

舉例:

execution(public * com.rainbowsea.mall.service.*.delete*(..))
 // 表示 public 公開的, * 返回值隨意,在 com.rainbowsea.mall.service.* 包下以及子包下的類, delete開頭的方法名的方法(引數隨意(個數任意,引數型別任意)),的方法。
 
execution(* com.rainbowsea.mall..*(..))
// 表示 許可權符(省略,則預設是包含了4種許可權了), * 返回值隨意,com.rainbowsea.mall 包下以及子包下的 ..* 任意類,任意方法名,方法中的引數(個數任意,引數型別任意)
execution(* *(..))
// 表示所有的類當中的所有方法,就是所有

3. 使用Spring 對 AOP 的實現使用

Spring 對AOP 的實現包括以下 三種方式:

  1. 第一種方式:Spring 框架結合 AspectJ框架實現的AOP,基於註解方式(該比較常用)。
  2. 第二種方式:Spring框架結合AspectJ框架實現的AOP,基於XML方式。
  3. 第三種方式:Spring 框架自己實現的AOP,基於XML方式。(並不怎麼用)

實際開發中,都是Spring + AspectJ實現AOP。所以我們重點學習使用第一種和第二種方式。

什麼是AspectJ ? (Eclipse組織的一個支援AOP的框架。AspectJ框架是獨立於Spring框架之外的一個框架,Spring框架用了AspectJ)
AspectJ專案起源於帕洛阿爾託(Palo Alto)研究中心(縮寫為PARC)。該中心由Xerox集團資助,Gregor Kiczales領導,從1997年開始致力於AspectJ的開發,1998年第一次釋出給外部使用者,2001年釋出1.0 release。為了推動AspectJ技術和社團的發展,PARC在2003年3月正式將AspectJ專案移交給了Eclipse組織,因為AspectJ的發展和受關注程度大大超出了PARC的預期,他們已經無力繼續維持它的發展。

3.1 準備工作

使用Spring+AspectJ的AOP需要引入的依賴如下:

在這裡插入圖片描述


<!--    Spring 的倉庫-->
    <repositories>
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>


    <dependencies>
        <!--        spring context 依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>


        <!--        spring aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.0.11</version>
        </dependency>
        <!--spring aspects依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.11</version>
        </dependency>

這邊,我多用上了一個 junit4 的註解

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>spring6-012-aop-realapp</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>


<!--    Spring 的倉庫-->
    <repositories>
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>


    <dependencies>
        <!--        spring context 依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>


        <!--        spring aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.0.11</version>
        </dependency>
        <!--spring aspects依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.11</version>
        </dependency>

        <!-- junit4 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

同時需要給:Spring.xml 配置檔案中新增 context 名稱空間和 aop 名稱空間:

在這裡插入圖片描述

<?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:context="http://www.springframework.org/schema/context"
       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/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>

3.2 Spring 基於AspectJ的AOP註解式開發

準備工作,搞定之後,我們就可以進行 Spring 基於 AspectJ的 AOP 註解式開發了。

3.2.1 實現步驟

第一步: 定義好目標類和目標方法。如下:

在這裡插入圖片描述


import org.springframework.stereotype.Service;

@Service(value = "orderService")  // 使用 @Service 註解,將該Bean交給 Spring IOC 容器管理
public class OrderService {  // 目標物件

    // 生成訂單業務方法
    public void generate() {  // 目標方法
        System.out.println("正在生成訂單...");
    }


    // 取消訂單的業務的方法
    public void cancel() {  // 目標方法
        System.out.println("訂單已取消");

    }
}

第二步: 定義切面類。

注意: 切面類也是要納入Spirng IOC 容器當中管理的,因為你是在Spring 框架當中運用的AOP 程式設計,當然,需要被Spring 管理到,Spring 管理不到,又該讓它如何使用呢。

目標類和切面類都納入spring bean管理
在目標類OrderService上新增@Component 註解。
在切面類MyAspect類上新增@Component 註解。

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.springframework.stereotype.Component;

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
public class MyAspect {

}

第四步: 在spring配置檔案中新增組建掃描

所謂的元件掃描:就是讓我們裡面的註解的位置,簡單的說:就是讓Spring 框架去哪些包下找,我們的註解,從而管理我們註解下的 Bean 類物件。

在這裡插入圖片描述

<?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:context="http://www.springframework.org/schema/context"
       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/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">


<!--    元件掃描-->
    <context:component-scan base-package="com.rainbowsea.spring6.service"></context:component-scan>

</beans>

第五步: 在切面類中新增通知

在切面類當中(對應的類當中)新增通知,就是將 @Aspect 註解,新增到類上就可以了,便會被Spring 框架開啟事務通知的操作。

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)
    public void advice(){
        System.out.println("我是一個通知");
    }

}

第六步:在通知上新增切點表示式

在方法當中新增上 @Before() 前置通知註解(詳細內容,文章後面說明),想要使用該@Before() 註解的前提是,要在該切面類上新增上 @Aspect 開啟事務的註解才行, @Before(execution編寫切點表示式)。

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)
    @Before("execution(* com.rainbowsea.spring6.service..*(..))")    // 切面表示式:表示:許可權符省略,任意返回值,該包下的以及包下的任意方法(引數型別,個人數任意)
    public void advice(){
        System.out.println("我是一個通知");
    }

}

第七步: 在spring配置檔案中啟用自動代理

在這裡插入圖片描述

<?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:context="http://www.springframework.org/schema/context"
       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/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">


<!--    元件掃描-->
    <context:component-scan base-package="com.rainbowsea.spring6.service"></context:component-scan>


<!--    啟動自動代理-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>
<aop:aspectj-autoproxy  proxy-target-class="true"/> 開啟自動代理之後,凡事帶有@Aspect註解的bean都會生成代理物件。
proxy-target-class="true" 表示採用cglib動態代理
proxy-target-class="false" 表示採用jdk動態代理。預設值是false。即使寫成false,當沒有介面的時候,也會自動選擇cglib生成代理類。

第八步: 測試程式:

在這裡插入圖片描述


import com.rainbowsea.spring6.service.AccountService;
import com.rainbowsea.spring6.service.OrderService;
import com.rainbowsea.spring6.service.UserService;
import com.rainbowsea.spring6.service.VipService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPRealAppTest {


    @Test
    public void testAOP() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
        AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
        accountService.transfer();

        accountService.withdraw();

    }
}

3.2.2 各個通知型別的說明

在Spring 的 AOP當中:有如下五種通知型別:

  1. 前置通知:@Before 目標方法執行之前的通知
  2. 後置通知:@AfterReturning 目標方法執行之後的通知
  3. 環繞通知:@Around 目標方法之前新增通知,通知目標方法執行之後新增通知
  4. 異常通知:@AfterThrowing 發生異常之後執行的通知
  5. 最終通知:@After 放在 finally 語句塊當中的通知

注意:以上五種通知,可以配合上,同時使用上

注意:想要運用通知,需要在對應的切面類上(通知類)上,新增上 @Aspect 註解,開啟事務通知,才行

  1. 前置通知:@Before 目標方法執行之前的通知

前置通知使用: @Before(切面表示式)

在這裡插入圖片描述

import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)

    // 前置通知
    @Before("execution(* com.rainbowsea.spring6.service..*(..))")
    public void beforeAdvice() {
        System.out.println("前置通知");
    }
}

在這裡插入圖片描述

  1. 後置通知:@AfterReturning 目標方法執行之後的通知

前置通知使用: @AfterReturning(切面表示式)

在這裡插入圖片描述

import org.springframework.stereotype.Component;

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)
    
    // 後置通知
    @AfterReturning("execution(* com.rainbowsea.spring6.service..*(..))")
    public void afterReturningAdvice() {
        System.out.println("後置通知");
    }
}

在這裡插入圖片描述

  1. 環繞通知:@Around 目標方法之前新增通知,通知目標方法執行之後新增通知

環繞通知需要加上:public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable,ProceedingJoinPoint joinPoint 引數,同時需要把 異常丟擲去,在環繞通知當中需要:自己手動透過 : ProceedingJoinPoint joinPoint.procced() 呼叫目標方法。才會執行目標方法,不然,是不會執行目標類當中的目標方法的。

在這裡插入圖片描述

// 這個JoinPoint joinPoint ,在Spring容器呼叫這個方法的時候自動傳過來
// 我們可以直接用,用這個 JoinPoint joinPoint 幹啥?
// Signature signature = joinPoint.getSignature(); 獲取目標方法的簽名
// 透過方法的簽名可以獲取到一個方法的具體資訊
// 獲取目標方法的方法名

環繞通知使用: @Around(切面表示式)

在這裡插入圖片描述


/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)

    // 環繞通知(環繞是最大的通知,在前置通知之前,在後置通知之後)
    @Around("execution(* com.rainbowsea.spring6.service..*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 前面的程式碼
        System.out.println("前環繞");

        // 執行目標
        joinPoint.proceed(); // 執行目標

        // 後面的程式碼
        System.out.println("後環繞");
    }
}

在這裡插入圖片描述

  1. 異常通知:@AfterThrowing 發生異常之後執行的通知,沒發生異常是不會執行的。

異常通知使用: @AfterThrowing(切面表示式)

在這裡插入圖片描述


/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)
    // 異常通知
    @AfterThrowing("execution(* com.rainbowsea.spring6.service..*(..))")
    public void afterThrowignAdvice() {
        System.out.println("異常通知");
    }
}

異常通知:沒有異常,就不會執行異常通知的操作。

在這裡插入圖片描述

這裡,我們新增模擬上一個 null 指標異常進行,測試異常通知的執行。

在這裡插入圖片描述

在這裡插入圖片描述

  1. 最終通知:@After 放在 finally 語句塊當中的通知

最終通知使用: @After(切面表示式)。最終通知:無論是否存在異常都是在最後都會執行的。

在這裡插入圖片描述

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)

    // 最終通知(finally 語句塊中的通知)
    @After(("execution(* com.rainbowsea.spring6.service..*(..))"))
    public void afterAdvice() {
        System.out.println("最終通知");
    }
}

在這裡插入圖片描述

有異常,最終通知,也是會執行的。

在這裡插入圖片描述

透過測試得知,當發生異常之後,最終通知也會執行,因為最終通知@After會出現在finally語句塊中。
出現異常之後,後置通知環繞通知的結束部分不會執行。

下面,我們集合五種通知型別,同時發生,看看他們在通知當中的執行順序又是如何的

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)

    // 前置通知
    @Before("execution(* com.rainbowsea.spring6.service..*(..))")
    public void beforeAdvice() {
        System.out.println("前置通知");
    }


    // 後置通知
    @AfterReturning("execution(* com.rainbowsea.spring6.service..*(..))")
    public void afterReturningAdvice() {
        System.out.println("後置通知");
    }


    // 環繞通知(環繞是最大的通知,在前置通知之前,在後置通知之後)
    @Around("execution(* com.rainbowsea.spring6.service..*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 前面的程式碼
        System.out.println("前環繞");

        // 執行目標
        joinPoint.proceed(); // 執行目標

        // 後面的程式碼
        System.out.println("後環繞");
    }


    // 異常通知
    @AfterThrowing("execution(* com.rainbowsea.spring6.service..*(..))")
    public void afterThrowignAdvice() {
        System.out.println("異常通知");

    }


    // 最終通知(finally 語句塊中的通知)
    @After(("execution(* com.rainbowsea.spring6.service..*(..))"))
    public void afterAdvice() {
        System.out.println("最終通知");
    }

}

在這裡插入圖片描述

有異常的:

在這裡插入圖片描述

前環繞
前置通知
銀行賬戶正在完成轉賬操作...
後置通知
最終通知
後環繞

總結:

環繞通知是範圍最大的(也是是說,環繞通知的(前環繞)是在所有通知的最前面執行的,而環繞通知的(後環繞)是在所有通知的最後面執行的)。

前環繞
前置通知
異常通知
最終通知

存在異常時,異常通知執行了,但是後面的:後置通知,環繞通知的(後環繞)通知,並不會執行,被異常給中斷了。

3.2.3 切面的先後順序的設定

我們知道,業務流程當中不一定只有一個切面,可能有的切面控制事務,有的記錄日誌,有的進安全控制,如果多個切面的話,順序如何控制:關於這一點:我們可以使用@Order 註解來標識切面類,為@Order註解的 value 指定一個整數型的數字,數字越小,優先順序越高。

在這裡插入圖片描述

這裡我們分別定義兩個通知:分別為 安全方面的通知,和日誌方面的通知。如下:

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;


/**
 * 安全事務
 */
@Component // 將該 Bean 交給Spring IOC 容器管理上
@Aspect // 開啟事務
public class SecureAspect {

    // 前置通知
    // 安全
    @Before("execution(* com.rainbowsea.spring6.service..*(..))")
    public void beforeAdvice() {
        System.out.println("安全方面的:前置通知");
    }
}

package com.rainbowsea.spring6.service;


import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;


/**
 * 日誌通知
 */
@Component // 將該 Bean 加入到 Spring IOC 容器當中管理
@Aspect  // 開啟事務
public class LogAspect {

    // 前置通知
    @Before("execution(* com.rainbowsea.spring6.service..*(..))")
    public void beforeAdvice() {
        System.out.println("日誌的通知方面的:前置通知");
    }
}

沒有新增@Order 註解的通知順序如下:

在這裡插入圖片描述

我們新增@Order註解的整數值來切換順序,這裡,我們將安全方面的通知,放在最前面執行,執行測試程式:

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


/**
 * 日誌通知
 */
@Component // 將該 Bean 加入到 Spring IOC 容器當中管理
@Aspect  // 開啟事務
@Order(2)  // 數值越小,優先順序越高,越先執行
public class LogAspect {

    // 前置通知
    @Before("execution(* com.rainbowsea.spring6.service..*(..))")
    public void beforeAdvice() {
        System.out.println("日誌的通知方面的:前置通知");
    }
}

package com.rainbowsea.spring6.service;


import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


/**
 * 安全事務
 */
@Component // 將該 Bean 交給Spring IOC 容器管理上
@Aspect // 開啟事務
@Order(1) // 數值越小,優先順序越高,越先執行
public class SecureAspect {

    // 前置通知
    // 安全
    @Before("execution(* com.rainbowsea.spring6.service..*(..))")
    public void beforeAdvice() {
        System.out.println("安全方面的:前置通知");
    }
}

在這裡插入圖片描述

3.2.4 最佳化切點表示式的方式

觀看以下程式碼中的切點表示式:

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)

    // 前置通知
    // 安全
    @Before("execution(* com.rainbowsea.spring6.service..*(..))")
    public void beforeAdvice() {
        System.out.println("前置通知");
    }


    // 後置通知
    @AfterReturning("execution(* com.rainbowsea.spring6.service..*(..))")
    public void afterReturningAdvice() {
        System.out.println("後置通知");
    }


    // 環繞通知(環繞是最大的通知,在前置通知之前,在後置通知之後)
    @Around("execution(* com.rainbowsea.spring6.service..*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 前面的程式碼
        System.out.println("前環繞");

        // 執行目標
        joinPoint.proceed(); // 執行目標

        // 後面的程式碼
        System.out.println("後環繞");
    }


    // 異常通知
    @AfterThrowing("execution(* com.rainbowsea.spring6.service..*(..))")
    public void afterThrowignAdvice() {
        System.out.println("異常通知");

    }


    // 最終通知(finally 語句塊中的通知)
    @After(("execution(* com.rainbowsea.spring6.service..*(..))"))
    public void afterAdvice() {
        System.out.println("最終通知");
    }

}

上述的缺點是:

  1. 第一:切點表示式重複寫了多次,沒有得到複用。
  2. 第二:如果要修改切點表示式,需要修改多處,難維護。

可以這樣做:將切點表示式單獨的定義出來,在需要的位置引入即可,如下:

我們可以使用:@Pointcut 註解來定義獨立的切點表示式。
注意這個 @Pointcut 註解標註的方法隨意,只是起到一個能夠讓@Pointcut註解編寫的位置。

在這裡插入圖片描述

在這裡插入圖片描述

在這裡插入圖片描述

package com.rainbowsea.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 切面類
 */
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
    // 這就是需要增強的程式碼(通知)

    // 定義通用的切點表示式
    @Pointcut("execution(* com.rainbowsea.spring6.service..*(..))")
    public void pointcut(){
        // 這個方法只是一個標記,方法名隨意,方法體也不需要寫任何程式碼。
    }

    // 前置通知
    // 安全
    @Before("pointcut()")
    public void beforeAdvice() {
        System.out.println("前置通知");
    }


    // 後置通知
    @AfterReturning("pointcut()")
    public void afterReturningAdvice() {
        System.out.println("後置通知");
    }


    // 環繞通知(環繞是最大的通知,在前置通知之前,在後置通知之後)
    @Around("pointcut()")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 前面的程式碼
        System.out.println("前環繞");

        // 執行目標
        joinPoint.proceed(); // 執行目標

        // 後面的程式碼
        System.out.println("後環繞");
    }


    // 異常通知
    @AfterThrowing("pointcut()")
    public void afterThrowignAdvice() {
        System.out.println("異常通知");

    }


    // 最終通知(finally 語句塊中的通知)
    @After(("pointcut()"))
    public void afterAdvice() {
        System.out.println("最終通知");
    }

}

在這裡插入圖片描述

3.2.5 Spring 全註解式開發ACP

就是編寫一個類,在這個類上面使用大量註解來代替 spring.xml的配置檔案,spring配置檔案消失了,如下:

@Configuration // 代替spring.xml 檔案
@ComponentScan(value = {"com.rainbowsea.spring6.service"})  // 元件掃描
@EnableAspectJAutoProxy(proxyTargetClass = true) // 啟用 aspectj的自動代理機制

在這裡插入圖片描述

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration // 代替spring.xml 檔案
@ComponentScan(value = {"com.rainbowsea.spring6.service"})  // 元件掃描
@EnableAspectJAutoProxy(proxyTargetClass = true) // 啟用 aspectj的自動代理機制
public class Spring6Config {

}

測試程式也變化了:因為我這裡是透過定義的一個配置類 代替 spring.xml 檔案的需要用的是:new AnnotationConfigApplicationContext(配置類.class),而不再是: new ClassPathXmlApplicationContext()了。

在這裡插入圖片描述

public class AOPRealAppTest {


    @Test
    public void testAOPWithAllAnnotation() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
        AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
        accountService.transfer();
    }

在這裡插入圖片描述

3.3 Spring 基於XML配置方式的AOP(瞭解)

第一步: 編寫目標類。因為這裡我們用的是 xml 配置方式的,不是註解的方式,所有就不用註解了。

在這裡插入圖片描述

package com.rainbowsea.spring6.service;

public class UserService {  // 目標物件

    public void logout() {  // 目標方法
        System.out.println("系統正在安全退出...");
    }
}

第二步: 編寫切面類,並且編寫通知

在這裡插入圖片描述

第三步: 編寫spring配置檔案

在這裡插入圖片描述

在這裡插入圖片描述

<?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:context="http://www.springframework.org/schema/context"
       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/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">


<!--納入 Spring ioc-->
    <bean id="userService" class="com.rainbowsea.spring6.service.UserService"></bean>
    <bean id="timerAspect" class="com.rainbowsea.spring6.service.TimerAspect"></bean>

<!--    aop 的配置-->
    <aop:config>
<!--        切點表示式-->
        <aop:pointcut id="mypointcut" expression="execution(* com.rainbowsea.spring6.service..*(..))"></aop:pointcut>
<!--        切面= 通知(具體程式碼)+切點(方法): 通知在方法裡(方法中可以寫具體的程式碼)-->
        <aop:aspect ref="timerAspect">
            <aop:around method="aroundAdvice" pointcut-ref="mypointcut"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

測試程式:

在這裡插入圖片描述

package com.rainbowsea.spring6.test;

import com.rainbowsea.spring6.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringAOPTest {


    @Test
    public void testXml() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.logout();
    }
}

3.4 AOP 的實際案例:事務處理

專案中的事務控制是在所難免的。在一個業務流程當中,可能需要多條DML語句共同完成,為了保證資料的安全,這多條DML語句要麼同時成功,要麼同時失敗。這就需要新增事務控制的程式碼。例如以下虛擬碼:

class 業務類1{
    public void 業務方法1(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
    public void 業務方法2(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
    public void 業務方法3(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
}

class 業務類2{
    public void 業務方法1(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
    public void 業務方法2(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
    public void 業務方法3(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
}
//......

可以看到,這些業務類中的每一個業務方法都是需要控制事務的,而控制事務的程式碼又是固定的格式,都是:

try{
    // 開啟事務
    startTransaction();

    // 執行核心業務邏輯
    //......

    // 提交事務
    commitTransaction();
}catch(Exception e){
    // 回滾事務
    rollbackTransaction();
}

這個控制事務的程式碼就是和業務邏輯沒有關係的 “交叉業務” 。以上虛擬碼當中可以看到這些交叉業務的程式碼沒有得到複用,並且如果這些交叉業務程式碼需要修改,那必然需要修改多處,難維護,怎麼解決?可以採用 AOP 思想解決。可以把以上控制事務的程式碼作為環繞通知 ,切入到目標類的方法當中,接下來我們做一下這件事,有兩個業務類,如下:在這裡插入圖片描述

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;


@Component(value = "accountService")
public class AccountService {  // 目標物件

    // 轉賬的業務方法
    public void transfer() {  // 目標方法
        System.out.println("銀行賬戶正在完成轉賬操作...");

    }


    // 取帳的業務方法
    public void withdraw() { // 目標方法
        System.out.println("正在取款,請稍後...");
    }
}

package com.rainbowsea.spring6.service;

import org.springframework.stereotype.Service;

@Service(value = "orderService")  // 使用 @Service 註解,將該Bean交給 Spring IOC 容器管理
public class OrderService {  // 目標物件

    // 生成訂單業務方法
    public void generate() {  // 目標方法
        System.out.println("正在生成訂單...");
    }


    // 取消訂單的業務的方法
    public void cancel() {  // 目標方法
        System.out.println("訂單已取消");

    }
}

在這裡插入圖片描述

注意,以上兩個業務類已經納入spring bean的管理,因為都新增了@Component註解。
接下來我們給以上兩個業務類的4個方法新增事務控制程式碼,使用AOP來完成:

package com.rainbowsea.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component  // 交給Spring 管理
@Aspect  // 開始AOP
public class TransactionAspect {
    // 程式設計式事務解決方案
    @Around("execution(* com.rainbowsea.spring6.service..*(..))")  // 環繞通知
    public void aroundAdvice(ProceedingJoinPoint joinPoint) {
        // 執行目標
        try {
            // 前環繞
            System.out.println("開啟事務");

            // 執行目標
            joinPoint.proceed();

            // 後環繞
            System.out.println("提交事務");

        } catch (Throwable throwable) {
            System.out.println("回滾事務");
            throwable.printStackTrace();
        }
    }
}

你看,這個事務控制程式碼是不是隻需要寫一次就行了,並且修改起來也沒有成本。編寫測試程式:

在這裡插入圖片描述

@Test
    public void testTransaction() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
        AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

        accountService.transfer();
        accountService.withdraw();

        orderService.generate();
        orderService.cancel();

    }

透過測試可以看到,所有的業務方法都新增了事務控制的程式碼。

關於Spring 對事務的支援使用,由於涉及到的篇幅比較多,大家可以移步至:✏️✏️✏️ (連結)

3.5 AOP 的實際案例:安全日誌

需求是這樣的:專案開發結束了,已經上線了。執行正常,客戶提出了新的需求:凡事在系統中進行修改操作的,刪除操作的,新增操作的,都要把這個人記錄下來。因為這幾個操作是屬於危險行為。

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.springframework.stereotype.Service;

@Service(value = "userService")  // 被spring 管理
public class UserService {
    public void saveUser() {
        System.out.println("新增使用者資訊");
    }



    public void deleteUser() {
        System.out.println("刪除使用者資訊");
    }


    public void modifyUser() {
        System.out.println("修改使用者資訊");
    }



    public void getUser() {
        System.out.println("獲取使用者資訊");
    }
}

在這裡插入圖片描述

package com.rainbowsea.spring6.service;


import org.springframework.stereotype.Service;

@Service(value = "vipService")  // 被 spring管理
public class VipService {
    public void saveVip() {
        System.out.println("新增Vip使用者資訊");
    }



    public void deleteVip() {
        System.out.println("刪除Vip使用者資訊");
    }


    public void modifyVip() {
        System.out.println("修改Vip使用者資訊");
    }



    public void getVip() {
        System.out.println("獲取Vip使用者資訊");
    }
}

注意:已經新增了@Component註解。
接下來我們使用aop來解決上面的需求:編寫一個負責安全的切面類。

這裡我們可以聯合使用,多個類當中的不同的方法名對應 AOP事務進行也給控制

在這裡插入圖片描述

package com.rainbowsea.spring6.service;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;


@Component(value = "securityLogAspect")  // 交給 spring 管理
@Aspect // 開啟事務
public class SecurityLogAspect {

    // 在 com.rainbowsea.spring6.service..save*() 包下的任意類當中的,save* 開頭的任意方法,引數任意
    @Pointcut("execution(* com.rainbowsea.spring6.service..save*(..))")
    public void savePointcut() {

    }

    // 在execution(* com.rainbowsea.spring6.service..delete*(..)) 包下的任意類當中的 delete*(..) 開頭的任意方法,任意引數
    @Pointcut("execution(* com.rainbowsea.spring6.service..delete*(..))")
    public void deletePointcut() {

    }

    // execution(* com.rainbowsea.spring6.service..delete*(..)) 包下的任意類當中的modify*(..) 開頭的任意方法,任意引數
    @Pointcut("execution(* com.rainbowsea.spring6.service..modify*(..))")
    public void modifyPointcut() {

    }





    // 聯合使用,多個類當中的不同的方法名對應 AOP事務
    @Before("savePointcut() || deletePointcut() || modifyPointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        // 系統時間
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss SSS");
        String nowTime = simpleDateFormat.format(new Date());

        // 輸出日誌資訊
        System.out.println(nowTime + "zhangsan" + joinPoint.getSignature().getDeclaringTypeName()+
                "." + joinPoint.getSignature().getName());
    }
}

在這裡插入圖片描述

在這裡插入圖片描述


    @Test
    public void testSecurityLong() {
        ApplicationContext applicationContext =  new ClassPathXmlApplicationContext("spring6.xml");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        VipService vipService = applicationContext.getBean("vipService", VipService.class);

        userService.saveUser();
        userService.deleteUser();
        userService.modifyUser();
        userService.getUser();


        vipService.saveVip();
        vipService.saveVip();
        vipService.modifyVip();
        vipService.getVip();
    }

4. 總結:

  1. 簡單的說AOP:就是將與核心業務 無關的程式碼抽離開來,形成一個獨立的元件,然後,以橫向 交叉的方式應用到業務流程當中的過程被稱為 AOP
  2. AOP的七大術語
  3. AOP 的五種通知(通知叫增強,就是具體你要插入\新增 的程式碼。)的特點:及其對應的註解
  4. AOP的切點表示式,所謂的切點表示式:就是用來定義通知(Advice) 往哪些方法上切入的。
execution([訪問控制許可權修飾符] 返回值型別 [全限定類名]方法名(形式引數列表) [異常])
  1. 切面類也是要納入Spirng IOC 容器當中管理的,因為你是在Spring 框架當中運用的AOP 程式設計,當然,需要被Spring 管理到,Spring 管理不到,又該讓它如何使用呢。
  2. 所有的五種通知上的使用都必須在該對應的類上,要在該切面類上新增上 @Aspect 開啟事務的註解才行。同時也要納入Spirng IOC 容器當中管理的。才行
  3. 五種通知,可以配合上,同時使用上時
    1. 環繞通知是範圍最大的(也是是說,環繞通知的(前環繞)是在所有通知的最前面執行的,而環繞通知的(後環繞)是在所有通知的最後面執行的)。
    2. 存在異常時,異常通知執行了,但是後面的:後置通知,環繞通知的(後環繞)通知,並不會執行,被異常給中斷了。
  4. 切面的先後順序的設定:關於這一點:我們可以使用@Order 註解來標識切面類,為@Order註解的 value 指定一個整數型的數字,數字越小,優先順序越高。
  5. 切點表示式的最佳化:我們可以使用:@Pointcut 註解來定義獨立的切點表示式。
    注意這個 @Pointcut 註解標註的方法隨意,只是起到一個能夠讓@Pointcut註解編寫的位置。
  6. 第一種方式:Spring 框架結合 AspectJ框架實現的AOP,基於註解方式(該比較常用)。
  7. 第二種方式:Spring框架結合AspectJ框架實現的AOP,基於XML方式。
  8. 關於Spring 在事務上的支援上的使用,由於涉及的篇幅內容過多,所以就不在這裡說明了,想要了解更多的可以移步至:✏️✏️✏️

切面類要不僅要加上 開始事務的註解,也要新增上 Spring IOC 容器管理的註解。

5. 最後:

“在這個最後的篇章中,我要表達我對每一位讀者的感激之情。你們的關注和回覆是我創作的動力源泉,我從你們身上吸取了無盡的靈感與勇氣。我會將你們的鼓勵留在心底,繼續在其他的領域奮鬥。感謝你們,我們總會在某個時刻再次相遇。”

在這裡插入圖片描述

相關文章