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介紹說明
一般一個系統當中都會有一些系統服務,例如:日誌,事務處理,安全等,這些系統服務被稱為:交叉業務 。這些交叉業務 幾乎是通用的,不管你是做銀行賬戶轉賬,還是刪除使用者資料。日誌,事務處理,安全,這些都是需要做的。
如果在每一個業務處理過程當中,都參雜這些交叉業務 程式碼進去的話,存在兩方面的問題:
- 第一:交叉業務程式碼在多個業務流程中反覆出現,顯然這個交叉業務 程式碼並沒有得到充分的複用,並且修改這些交叉業務 程式碼的話,需要修改多處。
- 第二:程式設計師無法專注核心業務程式碼的編寫,在編寫核心業務程式碼的同時還需要處理這些交叉業務。
使用 AOP 可以很輕鬆的解決以上問題。
如下圖:可以更好的理解 AOP思想。
簡單的說AOP:就是將與核心業務 無關的程式碼抽離開來,形成一個獨立的元件,然後,以橫向 交叉的方式應用到業務流程當中的過程被稱為 AOP 。
AOP的優點:
- 程式碼的複用性增強
- 程式碼易維護
- 使開發者更關注業務邏輯
2.1 AOP的七大術語
- 1 連線點 JoinPoint
在程式的整個執行流程中,可以切入 的位置,方法的執行前後,異常丟擲之後等位置。
- 2 切點 Pointcut
在程式執行流程中,真正織入 切面的方法。(一個切點對應多個連線點)
- 3 通知 Advice
通知叫增強,就是具體你要插入\新增 的程式碼。
通知包括:
- 前置通知
- 後置通知
- 環繞通知
- 異常通知
- 最終通知
- 4 切面 Aspect :切面 = 切點 + 通知*
- 5 織入 Weaving
把通知應用到目標物件的過程
- 6 代理物件 Proxy
一個目標物件被織入通知後產生的新物件
- 7 目標物件 Target
被織入通知的物件。
2.2 AOP 當中的 切點表示式
所謂的切點表示式:就是用來定義通知(Advice) 往哪些方法上切入的。
切入點表示式語法表達格式:
execution([訪問控制許可權修飾符] 返回值型別 [全限定類名]方法名(形式引數列表) [異常])
訪問控制許可權修飾符:
可選項,可以沒有 沒寫,就是預設包括了4個許可權 寫public 就表示只包括公開的方法。
- 返回值型別:
必須要有 “*” 表示返回值型別任意
- 全限定類名
可選項:可以不寫 “..” 兩個點,表示當前包以及子包下的所有類 省略:表示包括所有的類;就是所有
- 方法名
必填項 "*" 表示該對應包下的所有任意方法() 就是所有的方法 set* 則表示所有 set 開頭的方法() get* 則表示所有 get 開頭的方法()
- 形式引數列表
必填項 () 空括號,表示沒有引數的方法。 (..) 括號中有兩個點,表示引數型別和引數個數任意的方法, (*) 表示只有一個引數的方法 (*,String) 第一個引數型別隨意,第二個引數型別必須是String 型別才行
- 異常:
可選項 省略表示任意異常型別
舉例:
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 的實現包括以下 三種方式:
- 第一種方式:Spring 框架結合 AspectJ框架實現的AOP,基於註解方式(該比較常用)。
- 第二種方式:Spring框架結合AspectJ框架實現的AOP,基於XML方式。
- 第三種方式: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當中:有如下五種通知型別:
- 前置通知:
@Before
目標方法執行之前的通知- 後置通知:
@AfterReturning
目標方法執行之後的通知- 環繞通知:
@Around
目標方法之前新增通知,通知目標方法執行之後新增通知- 異常通知:
@AfterThrowing
發生異常之後執行的通知- 最終通知:
@After
放在 finally 語句塊當中的通知注意:以上五種通知,可以配合上,同時使用上
注意:想要運用通知,需要在對應的切面類上(通知類)上,新增上
@Aspect
註解,開啟事務通知,才行 。
- 前置通知:
@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("前置通知");
}
}
- 後置通知:
@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("後置通知");
}
}
- 環繞通知:
@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("後環繞");
}
}
- 異常通知:
@AfterThrowing
發生異常之後執行的通知,沒發生異常是不會執行的。
異常通知使用: @AfterThrowing(切面表示式)
/**
* 切面類
*/
@Component(value = "myAspect") // 納入 Spring IOC 容器當中管理
@Aspect // 開啟事務通知
public class MyAspect {
// 這就是需要增強的程式碼(通知)
// 異常通知
@AfterThrowing("execution(* com.rainbowsea.spring6.service..*(..))")
public void afterThrowignAdvice() {
System.out.println("異常通知");
}
}
異常通知:沒有異常,就不會執行異常通知的操作。
這裡,我們新增模擬上一個 null 指標異常進行,測試異常通知的執行。
- 最終通知:
@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("最終通知");
}
}
上述的缺點是:
- 第一:切點表示式重複寫了多次,沒有得到複用。
- 第二:如果要修改切點表示式,需要修改多處,難維護。
可以這樣做:將切點表示式單獨的定義出來,在需要的位置引入即可,如下:
我們可以使用:@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. 總結:
- 簡單的說AOP:就是將與核心業務 無關的程式碼抽離開來,形成一個獨立的元件,然後,以橫向 交叉的方式應用到業務流程當中的過程被稱為 AOP
- AOP的七大術語
- AOP 的五種通知(通知叫增強,就是具體你要插入\新增 的程式碼。)的特點:及其對應的註解
- AOP的切點表示式,所謂的切點表示式:就是用來定義通知(Advice) 往哪些方法上切入的。
execution([訪問控制許可權修飾符] 返回值型別 [全限定類名]方法名(形式引數列表) [異常])
- 切面類也是要納入Spirng IOC 容器當中管理的,因為你是在Spring 框架當中運用的AOP 程式設計,當然,需要被Spring 管理到,Spring 管理不到,又該讓它如何使用呢。
- 所有的五種通知上的使用都必須在該對應的類上,要在該切面類上新增上
@Aspect
開啟事務的註解才行。同時也要納入Spirng IOC 容器當中管理的。才行- 五種通知,可以配合上,同時使用上時
- 環繞通知是範圍最大的(也是是說,環繞通知的(前環繞)是在所有通知的最前面執行的,而環繞通知的(後環繞)是在所有通知的最後面執行的)。
- 存在異常時,異常通知執行了,但是後面的:後置通知,環繞通知的(後環繞)通知,並不會執行,被異常給中斷了。
- 切面的先後順序的設定:關於這一點:我們可以使用
@Order
註解來標識切面類,為@Order註解的 value 指定一個整數型的數字,數字越小,優先順序越高。- 切點表示式的最佳化:我們可以使用:
@Pointcut
註解來定義獨立的切點表示式。
注意這個 @Pointcut 註解標註的方法隨意,只是起到一個能夠讓@Pointcut註解編寫的位置。- 第一種方式:Spring 框架結合 AspectJ框架實現的AOP,基於註解方式(該比較常用)。
- 第二種方式:Spring框架結合AspectJ框架實現的AOP,基於XML方式。
- 關於Spring 在事務上的支援上的使用,由於涉及的篇幅內容過多,所以就不在這裡說明了,想要了解更多的可以移步至:✏️✏️✏️
切面類要不僅要加上 開始事務的註解,也要新增上 Spring IOC 容器管理的註解。
5. 最後:
“在這個最後的篇章中,我要表達我對每一位讀者的感激之情。你們的關注和回覆是我創作的動力源泉,我從你們身上吸取了無盡的靈感與勇氣。我會將你們的鼓勵留在心底,繼續在其他的領域奮鬥。感謝你們,我們總會在某個時刻再次相遇。”