輕鬆理解 Spring AOP

阿dun發表於2021-04-14

閱讀本文之前建議先了解動態代理,可以參看我的另一篇部落格 輕鬆理解 Java 靜態代理/動態代理

Spring AOP 簡介

Spring AOP 的基本概念

AOP (Aspect-Oriented Programming),即 面向切面程式設計, 它與 OOP (Object-Oriented Programming, 物件導向程式設計) 相輔相成, 提供了與 OOP 不同的抽象軟體結構的視角.

在 OOP 中, 我們以類(class)作為我們的基本單元, 而 AOP 中的基本單元是 Aspect(切面)

AOP是 Spring 是最難理解的概念之一,同時也是非常重要的知識點,因為它真的很常用。

面向切面程式設計

在面向切面程式設計的思想裡面,把功能分為兩種

  • 核心業務:登陸、註冊、增、刪、改、查、都叫核心業務
  • 周邊功能:日誌、事務管理這些次要的為周邊業務

在面向切面程式設計中,核心業務功能和周邊功能是分別獨立進行開發,兩者不是耦合的;

然後把切面功能和核心業務功能 "編織" 在一起,這就叫AOP

AOP 的目的

AOP能夠將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任(例如事務處理、日誌管理、許可權控制等)封裝起來,便於減少系統的重複程式碼降低模組間的耦合度,並有利於未來的可擴充性和可維護性

AOP 術語和流程

進一步瞭解AOP之前,我們先來看看AOP中使用到的一些術語,以及AOP執行的流程。

術語

  • 連線點(join point):對應的是具體被攔截的物件,因為Spring只能支援方法,所以被攔截的物件往往就是指特定的方法。具體是指一個方法
  • 切點(point cut):有時候,我們的切面不單單應用於單個方法,也可能是多個類的不同方法,這時,可以通過正則式和指示器的規則去定義,從而適配連線點。切點就是提供這樣一個功能的概念。具體是指具體共同特徵的多個方法。
  • 通知(advice):它會根據約定織入流程中,需要弄明白它們在流程中的順序和執行的條件,有這幾種:
    • 前置通知(before advice)
    • 環繞通知(around advice)
    • 後置通知(after advice)
    • 異常通知(afterThrowing advice)
    • 事後返回通知(afterReturning advice)
  • 目標物件(target):即被代理的物件,通俗理解各個切點的所在的類就是目標物件。
  • 引入(introduction):是指引入新的類和其方法,增強現有Bean的功能。
  • 織入(weaving):它是一個通過動態代理技術,為原有服務物件生成代理物件,然後將與切點定義匹配的連線點攔截,並按約定將各類通知織入約定流程的過程。
  • 切面(aspect):定義切點、各類通知和引入的內容,AOP將通過它的資訊來增強Bean的功能或將對應的方法織入流程。

上述的描述還是比較抽象的,配合下面的流程講解以及例子,應該充分掌握這些概念了。

流程

畫了一張圖,通過張圖可以清晰的瞭解AOP的整個流程,以及上面各個術語的意義和關係。

圖片的流程順序基於Spring 5

image-20210414091230633

五大通知執行順序

不同版本的Spring是有一定差異的,使用時候要注意

  • Spring 4

    • 正常情況:環繞前置 ==> @Before ==> 目標方法執行 ==> 環繞返回 ==> 環繞最終 ==> @After ==> @AfterReturning
    • 異常情況:環繞前置 ==> @Before ==> 目標方法執行 ==> 環繞異常 ==> 環繞最終 ==> @After ==> @AfterThrowing
  • Spring 5.28

    • 正常情況:環繞前置 ==> @Before ==> 目標方法執行 ==> @AfterReturning ==> @After ==> 環繞返回 ==> 環繞最終
    • 異常情況:環繞前置 ==> @Before ==> 目標方法執行 ==> @AfterThrowing ==> @After ==> 環繞異常 ==> 環繞最終

例子

圖例

舉一個實際中的例子來說明一下方便理解:

image-20210413110355452

房東的核心訴求其實就是籤合同,收錢,淺綠部分都是次要的,交給中介就好。

不過有的人可能就有疑問了,讓房東帶著不是更好嗎,租客溝通起來不是更輕鬆嗎?為啥非要分成兩部分呢?

那麼請看下面這種情況

image-20210413111018593

當我們有很多個房東的時候,中介的優勢就體現出來了。代入到我們實際的業務中,AOP能夠極大的減輕我們的開發工作,讓關注點程式碼與業務程式碼分離!實現解藕!

實際的程式碼

用一個實際程式碼案例來感受一下

  1. 建立一個房東
@Component("landlord")
public class Landlord {

	public void service() {
		System.out.println("籤合同");
		System.out.println("收錢");
	}
}
  1. 建立中介
@Component
@Aspect
class Broker {
	@Before("execution(* pojo.Landlord.service())")
	public void before(){
		System.out.println("帶租客看房");
		System.out.println("談錢");
	}
	@After("execution(* pojo.Landlord.service())")
	public void after(){
		System.out.println("給鑰匙");
	}
}

3.在 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: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="aspect" />
    <context:component-scan base-package="pojo" />
    <aop:aspectj-autoproxy/>
</beans>
  1. 測試
public class Test {
	public static void main(String[] args) {
		ApplicationContext context =
				new ClassPathXmlApplicationContext("applicationContext.xml");
		Landlord landlord = (Landlord) context.getBean("landlord", Landlord.class);
		landlord.service();
	}
}

5.執行看到效果:

帶租客看房
談錢
籤合同
收錢
給鑰匙

這個例子中我們用到了@Before@After兩個註解,其實就是設定的前置通知和後置通知。

最後的結果似乎與我們之前圖例中的順序不同,給鑰匙在收錢之後了,這個問題留到後面再解決,目前只需要簡單感受一下aop的使用即可。

預告:這種情況下應該使用環繞通知來完成這個需求


使用 Spring AOP

使用註解開發AOP

目前使用註解的方式進行Spring開發才是主流,包括SpringBoot中,已經是全註解開發,所以我們採用@AspectJ的註解方式,重新實現一下上面的用例,來學習AOP的使用。

註解 說明
@Before 前置通知,在連線點方法前呼叫
@Around 環繞通知,它將覆蓋原有方法,可以想象成前置+原方法+後置
@After 後置通知,在連線點方法後呼叫
@AfterReturning 返回通知,在連線點方法執行並正常返回後呼叫,要求連線點方法在執行過程中沒有發生異常
@AfterThrowing 異常通知,當連線點方法異常時呼叫

第一步:選擇連線點

Spring 是方法級別的 AOP 框架,我們主要也是以某個類額某個方法作為連線點,另一種說法就是:選擇哪一個類的哪一方法用以增強功能。

@Component
public class Landlord {
	public void service() {
		System.out.println("籤合同");
		System.out.println("收錢");
	}
}

我們在這裡就選擇上述 Landlord 類中的 service() 方法作為連線點。

第二步:建立切面

選擇好了連線點就可以建立切面了,我們可以把切面理解為一個攔截器,當程式執行到連線點的時候,被攔截下來,在開頭加入了初始化的方法,在結尾也加入了銷燬的方法而已,在 Spring 中只要使用 @Aspect 註解一個類,那麼 Spring IoC 容器就會認為這是一個切面了:

@Component
@Aspect
class Broker {

    @Before("execution(* com.aduner.demo03.pojo.Landlord.service())")
    public void before(){
        System.out.println("帶租客看房");
        System.out.println("談錢");
    }

    @After("execution(* com.aduner.demo03.pojo.Landlord.service())")
    public void after(){
        System.out.println("給鑰匙");
    }
}

切面的類仍然是一個 Bean ,需要 @Component 註解標註

在上面的註解中定義了 execution 的正規表示式,Spring會通過這個正則式去匹配、去確定對應的方法(連線點)是否啟用切面程式設計

execution(* com.aduner.demo03.pojo.Landlord.service())

依次對這個表示式作出分析:

  • execution:執行方法的時候會觸發
  • * :任意返回型別的方法
  • com.aduner.demo03.pojo.Landlord:類的全限定名
  • service():攔截的方法的名稱
  • 如果是service(*) 就是表示任意引數的service方法

第三步:定義切點

每一個註解都重複寫了同一個正則式,這顯然比較冗餘。為了克服這個問題,Spring定義了切點(Pointcut)的概念,切點的作用就是向Spring描述哪些類的哪些方法需要啟用AOP程式設計,這樣可以有效的降低程式碼的複雜度,而且有利於維護的方便。

@Component
@Aspect
class Broker {

    @Pointcut("execution(* com.aduner.demo03.pojo.Landlord.service())")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void before() {
        System.out.println("帶租客看房");
        System.out.println("談錢");
    }

    @After("pointcut()")
    public void after() {
        System.out.println("給鑰匙");
    }
}

第四步:配置好config

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.aduner.demo03.*",
        excludeFilters = {@ComponentScan.Filter(classes = {Service.class})})
public class AppConfig {

}

第五步:測試 AOP

@Test
void testAspect(){
    ApplicationContext ctx = new AnnotationConfigApplicationContext( AppConfig.class ) ;
    Landlord landlord=ctx.getBean(Landlord.class);
    landlord.service();
    ((ConfigurableApplicationContext)ctx).close();
}

結果

……
帶租客看房
談錢
籤合同
收錢
給鑰匙
……

環繞通知

現在我們來解決一下前面遺留的那個問題,收錢和給鑰匙的問題。

我們需要的應該是給了鑰匙之後再收錢,但是現在是反過來的。

要實現這個需求,用到環繞通知,這是 Spring AOP 中最強大的通知,整合了前置通知和後置通知。

環繞通知(Around)是所有通知中最為強大的通知,強大也意味著難以控制。一般而言,使用它的場景是在你需要大幅度修改原有目標物件的服務邏輯時,否則都儘量使用其他的通知。

環繞通知是一個取代原有目標物件方法的通知,當然它也提供了回撥原有目標物件方法的能力。

  1. 我們先來修改一下Landlord
@Component
public class Landlord {
    public void service(int steps) {
        if (steps == 1) {
            System.out.println("籤合同");
        } else if(steps==2){
            System.out.println("收錢");
        }
        else {
            System.out.println("籤合同");
            System.out.println("收錢");
        }

    }
}

我們將service新增一個引數,第一步籤合同,第二部收錢,如果沒有制定第一步或者第二步,就一起執行。

  1. 然後重新編寫一下我們的切面
@Component
@Aspect
class Broker {
    @Pointcut("execution(* com.aduner.demo03.pojo.Landlord.service(*))")
    public void pointcut() {
    }
  
    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) {
        System.out.println("帶租客看房");
        System.out.println("談價格");

        try {
          	// joinPoint.proceed(); 這樣就是執行原方法
            joinPoint.proceed(new Object[]{1}); //重新指定方法的引數
            System.out.println("交鑰匙");
            joinPoint.proceed(new Object[]{2});
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}
  1. 修改一下剛剛的測試類,給到一個初始引數
@Test
void testAspect(){
  ApplicationContext ctx = new AnnotationConfigApplicationContext( AppConfig.class ) ;
  Landlord landlord=ctx.getBean(Landlord.class);
  landlord.service(0);
  ((ConfigurableApplicationContext)ctx).close();
}

執行!成功!

……
帶租客看房
談價格
籤合同
交鑰匙
收錢
……

ProceedingJoinPoint物件

注意到切面編寫中Around裡面try中的joinPoint.proceed()方法

ProceedingJoinPoint物件是JoinPoint的子介面,該物件只用在@Around的切面方法中,新增了以下兩個方法。

Object proceed() throws Throwable //執行目標方法 
Object proceed(Object[] var1) throws Throwable //傳入的新的引數去執行目標方法 

前面的例子中我們顯示把房東的工作分為了兩步,然後再環繞通知中重新賦予引數並呼叫了兩次,在兩次中間插入了中介的工作。

實際開發中,上面這樣的寫法其實又會造成新的耦合,而且還會造成其他通知的混亂(呼叫了兩次方法,其實會讓有些通知返回兩次)。

當然這只是一個例子,為了幫助更好的理解環繞通知。

多個切面

Spring可以支援多個切面同時執行,如果剛好多個切面的切點相同,切面的執行順序就是一個關鍵了。

預設情況下,切面的執行順序是混亂的,如果需要指定切面的執行順序,我們需要用到@Order註解

@Component
@Aspect
@Order(1)
public class FirstAspect {
  ……
}
--------------------
@Component
@Aspect
@Order(2)
public class SecondAspect {
  ……
}

@Order註解中的值就是切片的順序,但是他們不是順序執行的而是包含關係。

image-20210414092656057

總結

  • AOP的出現是為了對程式解耦,減少系統的重複程式碼,提高可擴充性和可維護性
  • 常見的應用場景有許可權管理、快取、記錄跟蹤、優化、校準、日誌、事務等等等等……總之AOP的使用是非常常見的。
  • 需要注意不同Spring版本之間的AOP通知順序是有差別的。補充:Spring5.28為分界線。
  • 環繞通知很靈活、強大,但是也就意味著很難控制,如非必要,優先使用其他通知來完成。
  • 多切面作用同一個切點時候注意切片順序。

相關文章