javascript
Spring IOC 容器源码分析系列文章导读
1. 簡介
前一段時間,我學習了 Spring IOC 容器方面的源碼,并寫了數篇文章對此進行講解。在寫完 Spring IOC 容器源碼分析系列文章中的最后一篇后,沒敢懈怠,趁熱打鐵,花了3天時間閱讀了 AOP 方面的源碼。開始以為 AOP 部分的源碼也會比較復雜,所以原計劃投入一周的時間用于閱讀源碼。但在我大致理清 AOP 源碼邏輯后,發現沒想的那么復雜,所以目前進度算是超前了。從今天(5.15)開始,我將對 AOP 部分的源碼分析系列文章進行更新。包括本篇文章在內,本系列大概會有4篇文章,我將會在接下來一周時間內陸續進行更新。在本系列文章中,我將會分析 Spring AOP 是如何為 bean 篩選合適的通知器(Advisor),以及代理對象生成的過程。除此之外,還會對攔截器的調用過程進行分析。與前面的文章一樣,本系列文章不會對 AOP 的 XML 配置解析過程進行分析。
下面來講講本篇文章的內容,在本篇文章中,我將會向大家介紹一下 AOP 的原理,以及 AOP 中的一些術語及其對應的源碼。我覺得,大家在閱讀 AOP 源碼時,一定要弄懂這些術語和源碼。不然,在閱讀 AOP 源碼的過程中,可能會有點暈。好了,其他的就不多說了,下面進入正題吧。
?2. AOP 原理
關于 AOP 的原理,想必大家都知道了。無非是通過代理模式為目標對象生產代理對象,并將橫切邏輯插入到目標方法執行的前后。這樣一說,本章確實沒什么好說的了,畢竟原理就是這么簡單。不過原理歸原理,在具體的實現上,很多事情并沒想象的那么簡單。比如,我們需要確定是否應該為某個 bean 生成代理,如果應該的話,還要進一步確定將橫切邏輯插入到哪些方法上。說到橫切邏輯,這里簡單介紹一下。橫切邏輯其實就是通知(Advice),Spring 提供了5種通知,Spring 需要為每種通知提供相應的實現類。除了以上說的這些,在具體的實現過程中,還要考慮如何將 AOP 和 IOC 整合在一起,畢竟 IOC 是 Spring 框架的根基。除此之外,還有其他一些需要考慮的地方,這里就不一一列舉了。總之 AOP 原理說起來容易,但做起來卻不簡單,尤其是實現一個業界認可的,久經考驗的框架。所以,在隨后的文章中,讓我們帶著對代碼的敬畏之心,去學習 Spring AOP 模塊的源碼吧。
?3. AOP 術語及相應的實現
本章我來向大家介紹一下 AOP 中的一些術語,并會把這些術語對應的代碼也貼出來。在介紹這些術語之前,我們先來了解一下 AOP 吧。AOP 全稱是 Aspect Oriented Programming,即面向切面的編程,AOP 是一種開發理念。通過 AOP,我們可以把一些非業務邏輯的代碼,比如安全檢查,監控等代碼從業務方法中抽取出來,以非侵入的方式與原方法進行協同。這樣可以使原方法更專注于業務邏輯,代碼結構會更加清晰,便于維護。
這里特別說明一下,AOP 并非是 Spring 獨創,AOP 有自己的標準,也有機構在維護這個標準。Spring AOP 目前也遵循相關標準,所以別認為 AOP 是 Spring 獨創的。
?3.1 連接點 - Joinpoint
連接點是指程序執行過程中的一些點,比如方法調用,異常處理等。在 Spring AOP 中,僅支持方法級別的連接點。上面是比較官方的說明,下面舉個例子說明一下。現在我們有一個用戶服務 UserService 接口,該接口定義如下:
| 1 2 3 4 5 6 7 8 | public interface UserService {void save(User user);void update(User user);void delete(String userId);User findOne(String userId);List<User> findAll();boolean exists(String userId); } |
該接口的實現類是 UserServiceImpl,假設該類的方法調用如下:
如上所示,每個方法調用都是一個連接點。接下來,我們來看看連接點的定義:
| 1 2 3 4 5 6 7 8 9 10 | public interface Joinpoint {/** 用于執行攔截器鏈中的下一個攔截器邏輯 */Object proceed() throws Throwable;Object getThis();AccessibleObject getStaticPart();} |
這個 Joinpoint 接口中,proceed 方法是核心,該方法用于執行攔截器邏輯。關于攔截器這里簡單說一下吧,以前置通知攔截器為例。在執行目標方法前,該攔截器首先會執行前置通知邏輯,如果攔截器鏈中還有其他的攔截器,則繼續調用下一個攔截器邏輯。直到攔截器鏈中沒有其他的攔截器后,再去調用目標方法。關于攔截器這里先說這么多,在后續文章中,我會進行更為詳細的說明。
上面說到一個方法調用就是一個連接點,那下面我們不妨看一下方法調用這個接口的定義。如下:
| 1 2 3 4 5 6 7 | public interface Invocation extends Joinpoint {Object[] getArguments(); }public interface MethodInvocation extends Invocation {Method getMethod(); } |
如上所示,方法調用接口 MethodInvocation 繼承自 Invocation,Invocation 接口又繼承自 Joinpoint。看了上面的代碼,我想大家現在對連接點應該有更多的一些認識了。接下面,我們來繼續看一下 Joinpoint 接口的一個實現類 ReflectiveMethodInvocation。當然不是看源碼,而是看它的繼承體系圖。如下:
關于連接點的相關知識,我們先了解到這里。有了這些連接點,接下來要做的事情是對我們感興趣連接點進行一些橫切操作。在操作之前,我們首先要把我們所感興趣的連接點選中,怎么選中的呢?這就是切點 Pointcut 要做的事情了,繼續往下看。
?3.2 切點 - Pointcut
剛剛說到切點是用于選擇連接點的,那么應該怎么選呢?在回答這個問題前,我們不妨先去看看 Pointcut 接口的定義。如下:
| 1 2 3 4 5 6 7 8 9 10 | public interface Pointcut {/** 返回一個類型過濾器 */ClassFilter getClassFilter();/** 返回一個方法匹配器 */MethodMatcher getMethodMatcher();Pointcut TRUE = TruePointcut.INSTANCE; } |
Pointcut 接口中定義了兩個接口,分別用于返回類型過濾器和方法匹配器。下面我們再來看一下類型過濾器和方法匹配器接口的定義:
| 1 2 3 4 5 6 7 8 9 10 11 12 | public interface ClassFilter {boolean matches(Class<?> clazz);ClassFilter TRUE = TrueClassFilter.INSTANCE;}public interface MethodMatcher {boolean matches(Method method, Class<?> targetClass);boolean matches(Method method, Class<?> targetClass, Object... args);boolean isRuntime();MethodMatcher TRUE = TrueMethodMatcher.INSTANCE; } |
上面的兩個接口均定義了 matches 方法,用戶只要實現了 matches 方法,即可對連接點進行選擇。在日常使用中,大家通常是用 AspectJ 表達式對連接點進行選擇。Spring 中提供了一個 AspectJ 表達式切點類 - AspectJExpressionPointcut,下面我們來看一下這個類的繼承體系圖:
如上所示,這個類最終實現了 Pointcut、ClassFilter 和 MethodMatcher 接口,因此該類具備了通過 AspectJ 表達式對連接點進行選擇的能力。那下面我們不妨寫一個表達式對上一節的連接點進行選擇,比如下面這個表達式:
| 1 | execution(* *.find*(..)) |
該表達式用于選擇以 find 的開頭的方法,選擇結果如下:
通過上面的表達式,我們可以就可以選中 findOne 和 findAll 兩個方法了。那選中方法之后呢?當然是要搞點事情。so,接下來通知(Advice)就該上場了。
?3.3 通知 - Advice
通知 Advice 即我們定義的橫切邏輯,比如我們可以定義一個用于監控方法性能的通知,也可以定義一個安全檢查的通知等。如果說切點解決了通知在哪里調用的問題,那么現在還需要考慮了一個問題,即通知在何時被調用?是在目標方法前被調用,還是在目標方法返回后被調用,還在兩者兼備呢?Spring 幫我們解答了這個問題,Spring 中定義了以下幾種通知類型:
- 前置通知(Before advice)- 在目標方便調用前執行通知
- 后置通知(After advice)- 在目標方法完成后執行通知
- 返回通知(After returning advice)- 在目標方法執行成功后,調用通知
- 異常通知(After throwing advice)- 在目標方法拋出異常后,執行通知
- 環繞通知(Around advice)- 在目標方法調用前后均可執行自定義邏輯
上面是對通知的一些介紹,下面我們來看一下通知的源碼吧。如下:
| 1 2 3 | public interface Advice {} |
如上,通知接口里好像什么都沒定義。不過別慌,我們再去到它的子類接口中一探究竟。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /** BeforeAdvice */ public interface BeforeAdvice extends Advice {}public interface MethodBeforeAdvice extends BeforeAdvice {void before(Method method, Object[] args, Object target) throws Throwable; }/** AfterAdvice */ public interface AfterAdvice extends Advice {}public interface AfterReturningAdvice extends AfterAdvice {void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable; } |
從上面的代碼中可以看出,Advice 接口的子類接口里還是定義了一些東西的。下面我們再來看看 Advice 接口的具體實現類 AspectJMethodBeforeAdvice 的繼承體系圖,如下:
現在我們有了切點 Pointcut 和通知 Advice,由于這兩個模塊目前還是分離的,我們需要把它們整合在一起。這樣切點就可以為通知進行導航,然后由通知邏輯實施精確打擊。那怎么整合兩個模塊呢?答案是,切面。好的,是時候來介紹切面 Aspect 這個概念了。
?3.4 切面 - Aspect
切面 Aspect 整合了切點和通知兩個模塊,切點解決了 where 問題,通知解決了 when 和 how 問題。切面把兩者整合起來,就可以解決 對什么方法(where)在何時(when - 前置還是后置,或者環繞)執行什么樣的橫切邏輯(how)的三連發問題。在 AOP 中,切面只是一個概念,并沒有一個具體的接口或類與此對應。不過 Spring 中倒是有一個接口的用途和切面很像,我們不妨了解一下,這個接口就是切點通知器 PointcutAdvisor。我們先來看看這個接口的定義,如下:
| 1 2 3 4 5 6 7 8 9 10 | public interface Advisor {Advice getAdvice();boolean isPerInstance(); }public interface PointcutAdvisor extends Advisor {Pointcut getPointcut(); } |
簡單來說一下 PointcutAdvisor 及其父接口 Advisor,Advisor 中有一個 getAdvice 方法,用于返回通知。PointcutAdvisor 在 Advisor 基礎上,新增了 getPointcut 方法,用于返回切點對象。因此 PointcutAdvisor 的實現類即可以返回切點,也可以返回通知,所以說 PointcutAdvisor 和切面的功能相似。不過他們之間還是有一些差異的,比如看下面的配置:
| 1 2 3 4 5 6 7 8 9 10 11 12 | <bean id="aopCode" class="xyz.coolblog.aop.AopCode"/><aop:config expose-proxy="true"><aop:aspect ref="aopCode"><!-- pointcut --><aop:pointcut id="helloPointcut" expression="execution(* xyz.coolblog.aop.*.hello*(..))" /><!-- advoce --><aop:before method="before" pointcut-ref="helloPointcut"/><aop:after method="after" pointcut-ref="helloPointcut"/></aop:aspect> </aop:config> |
如上,一個切面中配置了一個切點和兩個通知,兩個通知均引用了同一個切點,即 pointcut-ref=“helloPointcut”。這里在一個切面中,一個切點對應多個通知,是一對多的關系(可以配置多個 pointcut,形成多對多的關系)。而在 PointcutAdvisor 的實現類中,切點和通知是一一對應的關系。上面的通知最終會被轉換成兩個 PointcutAdvisor,這里我把源碼調試的結果貼在下面:
在本節的最后,我們再來看看 PointcutAdvisor 的實現類 AspectJPointcutAdvisor 的繼承體系圖。如下:
?3.5 織入 - Weaving
現在我們有了連接點、切點、通知,以及切面等,可謂萬事俱備,但是還差了一股東風。這股東風是什么呢?沒錯,就是織入。所謂織入就是在切點的引導下,將通知邏輯插入到方法調用上,使得我們的通知邏輯在方法調用時得以執行。說完織入的概念,現在來說說 Spring 是通過何種方式將通知織入到目標方法上的。先來說說以何種方式進行織入,這個方式就是通過實現后置處理器 BeanPostProcessor 接口。該接口是 Spring 提供的一個拓展接口,通過實現該接口,用戶可在 bean 初始化前后做一些自定義操作。那 Spring 是在何時進行織入操作的呢?答案是在 bean 初始化完成后,即 bean 執行完初始化方法(init-method)。Spring通過切點對 bean 類中的方法進行匹配。若匹配成功,則會為該 bean 生成代理對象,并將代理對象返回給容器。容器向后置處理器輸入 bean 對象,得到 bean 對象的代理,這樣就完成了織入過程。關于后置處理器的細節,這里就不多說了.大家若有興趣,可以參考我之前寫的Spring IOC 容器源碼分析系列文章。
?4.總結
本篇文章作為 AOP 源碼分析系列文章的導讀,簡單介紹了 AOP 中的一些術語,及其對應的源碼。總的來說,沒有什么特別之處。畢竟對于 AOP,大家都有所了解。因此,若文中有不妥錯誤之處,還請大家指明。當然,也希望多多指教。
好了,本篇文章先到這里。感謝大家的閱讀。
?參考
- Spring Framework Reference Documentation
- 《Spring 實戰》第4版 - Craig Walls
?附錄:Spring 源碼分析文章列表
?Ⅰ. IOC
| 2018-05-30 | Spring IOC 容器源碼分析系列文章導讀 |
| 2018-06-01 | Spring IOC 容器源碼分析 - 獲取單例 bean |
| 2018-06-04 | Spring IOC 容器源碼分析 - 創建單例 bean 的過程 |
| 2018-06-06 | Spring IOC 容器源碼分析 - 創建原始 bean 對象 |
| 2018-06-08 | Spring IOC 容器源碼分析 - 循環依賴的解決辦法 |
| 2018-06-11 | Spring IOC 容器源碼分析 - 填充屬性到 bean 原始對象 |
| 2018-06-11 | Spring IOC 容器源碼分析 - 余下的初始化工作 |
?Ⅱ. AOP
| 2018-06-17 | Spring AOP 源碼分析系列文章導讀 |
| 2018-06-20 | Spring AOP 源碼分析 - 篩選合適的通知器 |
| 2018-06-20 | Spring AOP 源碼分析 - 創建代理對象 |
| 2018-06-22 | Spring AOP 源碼分析 - 攔截器鏈的執行過程 |
?Ⅲ. MVC
| 2018-06-29 | Spring MVC 原理探秘 - 一個請求的旅行過程 |
| 2018-06-30 | Spring MVC 原理探秘 - 容器的創建過程 |
- 本文鏈接:?https://www.tianxiaobo.com/2018/06/17/Spring-AOP-源碼分析系列文章導讀/
from:?http://www.tianxiaobo.com/2018/06/17/Spring-AOP-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0%E5%AF%BC%E8%AF%BB/?
總結
以上是生活随笔為你收集整理的Spring IOC 容器源码分析系列文章导读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring IOC 容器源码分析 -
- 下一篇: Spring AOP 源码分析 - 筛选