76 張圖,剖析 Spring AOP 原始碼,小白居然也能看懂,大神,請收下我的膝蓋!

語言: CN / TW / HK

預計閱讀 30 分鐘,建議先收藏~~

大家好,我是樓仔!

前兩篇分享的 Spring 原始碼,反響非常不錯,這個是原始碼系列的第 3 篇

前兩篇的原始碼解析,涉及到很多基礎知識,但是原始碼的解讀都不難,這篇文章剛好相反,依賴的基礎知識不多,但是原始碼比較難懂。

下面我會簡單介紹一下 AOP 的基礎知識,以及使用方法,然後直接對原始碼進行拆解。

不 BB,上文章目錄。

1. 基礎知識

1.1 什麼是 AOP ?

AOP 的全稱是 “Aspect Oriented Programming”,即面向切面程式設計

在 AOP 的思想裡面,周邊功能(比如效能統計,日誌,事務管理等)被定義為切面,核心功能和切面功能分別獨立進行開發,然後把核心功能和切面功能“編織”在一起,這就叫 AOP。

AOP 能夠將那些與業務無關,卻為業務模組所共同呼叫的邏輯封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可拓展性和可維護性。

1.2 AOP 基礎概念

  • 連線點(Join point):能夠被攔截的地方,Spring AOP 是基於動態代理的,所以是方法攔截的,每個成員方法都可以稱之為連線點;
  • 切點(Poincut):每個方法都可以稱之為連線點,我們具體定位到某一個方法就成為切點;
  • 增強/通知(Advice):表示新增到切點的一段邏輯程式碼,並定位連線點的方位資訊,簡單來說就定義了是幹什麼的,具體是在哪幹;
  • 織入(Weaving):將增強/通知新增到目標類的具體連線點上的過程;
  • 引入/引介(Introduction):允許我們向現有的類新增新方法或屬性,是一種特殊的增強;
  • 切面(Aspect):切面由切點和增強/通知組成,它既包括了橫切邏輯的定義、也包括了連線點的定義。

上面的解釋偏官方,下面用“方言”再給大家解釋一遍。 - 切入點(Pointcut):在哪些類,哪些方法上切入(where); - 通知(Advice):在方法執行的什麼時機(when:方法前/方法後/方法前後)做什麼(what:增強的功能); - 切面(Aspect):切面 = 切入點 + 通知,通俗點就是在什麼時機,什麼地方,做什麼增強; - 織入(Weaving):把切面加入到物件,並創建出代理物件的過程,這個由 Spring 來完成。

5 種通知的分類: - 前置通知(Before Advice):在目標方法被呼叫前呼叫通知功能; - 後置通知(After Advice):在目標方法被呼叫之後呼叫通知功能; - 返回通知(After-returning):在目標方法成功執行之後呼叫通知功能; - 異常通知(After-throwing):在目標方法丟擲異常之後呼叫通知功能; - 環繞通知(Around):把整個目標方法包裹起來,在被呼叫前和呼叫之後分別呼叫通知功能。

1.3 AOP 簡單示例

新建 Louzai 類:

```java @Data @Service public class Louzai {

public void everyDay() {
    System.out.println("睡覺");
}

} ```

新增 LouzaiAspect 切面:

```java @Aspect @Component public class LouzaiAspect {

@Pointcut("execution(* com.java.Louzai.everyDay())")
private void myPointCut() {
}

// 前置通知
@Before("myPointCut()")
public void myBefore() {
    System.out.println("吃飯");
}

// 後置通知
@AfterReturning(value = "myPointCut()")
public void myAfterReturning() {
    System.out.println("打豆豆。。。");
}

} ```

applicationContext.xml 新增: ```

```

程式入口: java public class MyTest { public static void main(String[] args) { ApplicationContext context =new ClassPathXmlApplicationContext("classpath:applicationContext.xml"); Louzai louzai = (Louzai) context.getBean("louzai"); louzai.everyDay(); } }

輸出: 吃飯 睡覺 打豆豆。。。

這個示例非常簡單,“睡覺” 加了前置和後置通知,但是 Spring 在內部是如何工作的呢?

1.4 Spring AOP 工作流程

為了方便大家能更好看懂後面的原始碼,我先整體介紹一下原始碼的執行流程,讓大家有一個整體的認識,否則容易被繞進去。

整個 Spring AOP 原始碼,其實分為 3 塊,我們會結合上面的示例,給大家進行講解。

第一塊就是前置處理,我們在建立 Louzai Bean 的前置處理中,會遍歷程式所有的切面資訊,然後將切面資訊儲存在快取中,比如示例中 LouzaiAspect 的所有切面資訊。

第二塊就是後置處理,我們在建立 Louzai Bean 的後置處理器中,裡面會做兩件事情: - 獲取 Louzai 的切面方法:首先會從快取中拿到所有的切面資訊,和 Louzai 的所有方法進行匹配,然後找到 Louzai 所有需要進行 AOP 的方法。 - 建立 AOP 代理物件:結合 Louzai 需要進行 AOP 的方法,選擇 Cglib 或 JDK,建立 AOP 代理物件。

第三塊就是執行切面,通過“責任鏈 + 遞迴”,去執行切面。

2. 原始碼解讀

注意:Spring 的版本是 5.2.15.RELEASE,否則和我的程式碼不一樣!!!

除了原理部分,上面的知識都不難,下面才是我們的重頭戲,讓你跟著樓仔,走一遍程式碼流程。

2.1 程式碼入口

這裡需要多跑幾次,把前面的 beanName 跳過去,只看 louzai。

進入 doGetBean(),進入建立 Bean 的邏輯。

2.2 前置處理

主要就是遍歷切面,放入快取。

這裡是重點!敲黑板!!!

  1. 我們會先遍歷所有的類;
  2. 判斷是否切面,只有切面才會進入後面邏輯;
  3. 獲取每個 Aspect 的切面列表;
  4. 儲存 Aspect 的切面列表到快取 advisorsCache 中。

到這裡,獲取切面資訊的流程就結束了,因為後續對切面資料的獲取,都是從快取 advisorsCache 中拿到。

下面就對上面的流程,再深入解讀一下。

2.2.1 判斷是否是切面

上圖的第 2 步,邏輯如下:

2.2.2 獲取切面列表

進入到 getAdvice(),生成切面資訊。

2.3 後置處理

主要就是從快取拿切面,和 louzai 的方法匹配,並建立 AOP 代理物件。

進入 doCreateBean(),走下面邏輯。

這裡是重點!敲黑板!!!

  1. 先獲取 louzai 類的所有切面列表;
  2. 建立一個 AOP 的代理物件。

2.3.1 獲取切面

我們先進入第一步,看是如何獲取 louzai 的切面列表。

進入 buildAspectJAdvisors(),這個方法應該有印象,就是前面將切面資訊放入快取 advisorsCache 中,現在這裡就是要獲取快取。

再回到 findEligibleAdvisors(),從快取拿到所有的切面資訊後,繼續往後執行。

2.3.2 建立代理物件

有了 louzai 的切面列表,後面就可以開始去建立 AOP 代理物件。

這裡是重點!敲黑板!!!

這裡有 2 種建立 AOP 代理物件的方式,我們是選用 Cglib 來建立。

我們再回到建立代理物件的入口,看看建立的代理物件。

2.4 切面執行

通過 “責任鏈 + 遞迴”,執行切面和方法。

前方高能!這塊邏輯非常複雜!!!

下面就是“執行切面”最核心的邏輯,簡單說一下設計思路: 1. 設計思路:採用遞迴 + 責任鏈的模式; 2. 遞迴:反覆執行 CglibMethodInvocation 的 proceed(); 3. 退出遞迴條件:interceptorsAndDynamicMethodMatchers 陣列中的物件,全部執行完畢; 4. 責任鏈:示例中的責任鏈,是個長度為 3 的陣列,每次取其中一個數組物件,然後去執行物件的 invoke()。

因為我們數組裡面只有 3 個物件,所以只會遞迴 3 次,下面就看這 3 次是如何遞迴,責任鏈是如何執行的,設計得很巧妙!

2.4.1 第一次遞迴

陣列的第一個物件是 ExposeInvocationInterceptor,執行 invoke(),注意入參是 CglibMethodInvocation。

裡面啥都沒幹,繼續執行 CglibMethodInvocation 的 process()。

2.4.2 第二次遞迴

陣列的第二個物件是 MethodBeforeAdviceInterceptor,執行 invoke()。

2.4.3 第三次遞迴

陣列的第二個物件是 AfterReturningAdviceInterceptor,執行 invoke()。

執行完上面邏輯,就會退出遞迴,我們看看 invokeJoinpoint() 的執行邏輯,其實就是執行主方法。

再回到第三次遞迴的入口,繼續執行後面的切面。

切面執行邏輯,前面已經演示過,直接看執行方法。

後面就依次退出遞迴,整個流程結束。

2.4.4 設計思路

這塊程式碼,我研究了大半天,因為這個不是純粹的責任鏈模式。

純粹的責任鏈模式,物件內部有一個自身的 next 物件,執行完當前物件的方法末尾,就會啟動 next 物件的執行,直到最後一個 next 物件執行完畢,或者中途因為某些條件中斷執行,責任鏈才會退出。

這裡 CglibMethodInvocation 物件內部沒有 next 物件,全程是通過 interceptorsAndDynamicMethodMatchers 長度為 3 的陣列控制,依次去執行陣列中的物件,直到最後一個物件執行完畢,責任鏈才會退出。

這個也屬於責任鏈,只是實現方式不一樣,後面會詳細剖析,下面再討論一下,這些類之間的關係。

我們的主物件是 CglibMethodInvocation,繼承於 ReflectiveMethodInvocation,然後 process() 的核心邏輯,其實都在 ReflectiveMethodInvocation 中。

ReflectiveMethodInvocation 中的 process() 控制整個責任鏈的執行。

ReflectiveMethodInvocation 中的 process() 方法,裡面有個長度為 3 的陣列 interceptorsAndDynamicMethodMatchers,裡面儲存了 3 個物件,分別為 ExposeInvocationInterceptor、MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor。

注意!!!這 3 個物件,都是繼承 MethodInterceptor 介面。

然後每次執行 invoke() 時,裡面都會去執行 CglibMethodInvocation 的 process()。

是不是聽得有些蒙圈?甭著急,我重新再幫你梳理一下。

物件和方法的關係: - 介面繼承:陣列中的 3 個物件,都是繼承 MethodInterceptor 介面,實現裡面的 invoke() 方法; - 類繼承:我們的主物件 CglibMethodInvocation,繼承於 ReflectiveMethodInvocation,複用它的 process() 方法; - 兩者結合(策略模式):invoke() 的入參,就是 CglibMethodInvocation,執行 invoke() 時,內部會執行 CglibMethodInvocation.process(),這個其實就是個策略模式。

可能有同學會說,invoke() 的入參是 MethodInvocation,沒錯!但是 CglibMethodInvocation 也繼承了 MethodInvocation,不信自己可以去看。

執行邏輯: - 程式入口:是 CglibMethodInvocation 的 process() 方法; - 鏈式執行(衍生的責任鏈模式):process() 中有個包含 3 個物件的陣列,依次去執行每個物件的 invoke() 方法。 - 遞迴(邏輯回退):invoke() 方法會執行切面邏輯,同時也會執行 CglibMethodInvocation 的 process() 方法,讓邏輯再一次進入 process()。 - 遞迴退出:當數字中的 3 個物件全部執行完畢,流程結束。

所以這裡設計巧妙的地方,是因為純粹責任鏈模式,裡面的 next 物件,需要保證裡面的物件型別完全相同。

但是數組裡面的 3 個物件,裡面沒有 next 成員物件,所以不能直接用責任鏈模式,那怎麼辦呢?就單獨搞了一個 CglibMethodInvocation.process(),通過去無限遞迴 process(),來實現這個責任鏈的邏輯。

這就是我們為什麼要看原始碼,學習裡面優秀的設計思路!

3. 總結

我們再小節一下,文章先介紹了什麼是 AOP,以及 AOP 的原理和示例。

之後再剖析了 AOP 的原始碼,分為 3 塊: - 將所有的切面都儲存在快取中; - 取出快取中的切面列表,和 louzai 物件的所有方法匹配,拿到屬於 louzai 的切面列表; - 建立 AOP 代理物件; - 通過“責任鏈 + 遞迴”,去執行切面邏輯。

這篇文章,是 Spring 原始碼解析的第 3 篇,也是感覺最難的一篇,光圖解程式碼就扣了 6 個小時,整個人都被扣麻了。

最難的地方還不是摳圖,而是 “切面執行”的設計思路,雖然流程能走通,但是把整個設計思想能總結出來,並講得能讓大家明白,還是非常不容易的。

今天的原始碼解析就到這,Spring 相關的原始碼,還有哪些是大家想學習的呢,可以給樓仔留言。

這篇文章肝了我 2 個星期,原創不易,大家的點贊和分享,是我繼續創作的最大動力!


硬核推薦: