76 張圖,剖析 Spring AOP 源碼,小白居然也能看懂,大神,請收下我的膝蓋!
下面我會簡單介紹一下 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 類:
@Data
@Service
public class Louzai {
public void everyDay() {
System.out.println("睡覺");
}
}
添加 LouzaiAspect 切面:
@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 添加:
<!--啟用@Autowired等注解-->
<context:annotation-config/>
<context:component-scan base-package="com" />
<aop:aspectj-autoproxy proxy-target-class="true"/>
程序入口:
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 前置處理
主要就是遍歷切面,放入緩存。
這里是重點!敲黑板!!!
- 我們會先遍歷所有的類;
- 判斷是否切面,只有切面才會進入后面邏輯;
- 獲取每個 Aspect 的切面列表;
- 保存 Aspect 的切面列表到緩存 advisorsCache 中。
到這里,獲取切面信息的流程就結束了,因為后續對切面數據的獲取,都是從緩存 advisorsCache 中拿到。
下面就對上面的流程,再深入解讀一下。
2.2.1 判斷是否是切面
上圖的第 2 步,邏輯如下:
2.2.2 獲取切面列表
進入到 getAdvice(),生成切面信息。
2.3 后置處理
主要就是從緩存拿切面,和 louzai 的方法匹配,并創建 AOP 代理對象。
進入 doCreateBean(),走下面邏輯。
這里是重點!敲黑板!!!
- 先獲取 louzai 類的所有切面列表;
- 創建一個 AOP 的代理對象。
2.3.1 獲取切面
我們先進入第一步,看是如何獲取 louzai 的切面列表。
進入 buildAspectJAdvisors(),這個方法應該有印象,就是前面將切面信息放入緩存 advisorsCache 中,現在這里就是要獲取緩存。
再回到 findEligibleAdvisors(),從緩存拿到所有的切面信息后,繼續往后執行。
2.3.2 創建代理對象
有了 louzai 的切面列表,后面就可以開始去創建 AOP 代理對象。
這里是重點!敲黑板!!!
這里有 2 種創建 AOP 代理對象的方式,我們是選用 Cglib 來創建。
我們再回到創建代理對象的入口,看看創建的代理對象。
2.4 切面執行
通過 “責任鏈 + 遞歸”,執行切面和方法。
前方高能!這塊邏輯非常復雜!!!
下面就是“執行切面”最核心的邏輯,簡單說一下設計思路:
- ?設計思路:采用遞歸 + 責任鏈的模式;
- 遞歸:反復執行 CglibMethodInvocation 的 proceed();
- 退出遞歸條件:interceptorsAndDynamicMethodMatchers 數組中的對象,全部執行完畢;
- 責任鏈:示例中的責任鏈,是個長度為 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 代理對象;
- 通過“責任鏈 + 遞歸”,去執行切面邏輯。
最難的地方還不是摳圖,而是 “切面執行”的設計思路,雖然流程能走通,但是把整個設計思想能總結出來,并講得能讓大家明白,還是非常不容易的。