Spring AOP 源碼分析系列文章導讀


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 接口,該接口定義如下:

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,假設該類的方法調用如下:

如上所示,每個方法調用都是一個連接點。接下來,我們來看看連接點的定義:

public interface Joinpoint {

    /** 用於執行攔截器鏈中的下一個攔截器邏輯 */
    Object proceed() throws Throwable;

    Object getThis();

    AccessibleObject getStaticPart();

}

這個 Joinpoint 接口中,proceed 方法是核心,該方法用於執行攔截器邏輯。關於攔截器這里簡單說一下吧,以前置通知攔截器為例。在執行目標方法前,該攔截器首先會執行前置通知邏輯,如果攔截器鏈中還有其他的攔截器,則繼續調用下一個攔截器邏輯。直到攔截器鏈中沒有其他的攔截器后,再去調用目標方法。關於攔截器這里先說這么多,在后續文章中,我會進行更為詳細的說明。

上面說到一個方法調用就是一個連接點,那下面我們不妨看一下方法調用這個接口的定義。如下:

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 接口的定義。如下:

public interface Pointcut {

    /** 返回一個類型過濾器 */
    ClassFilter getClassFilter();

    /** 返回一個方法匹配器 */
    MethodMatcher getMethodMatcher();

    Pointcut TRUE = TruePointcut.INSTANCE;
}

Pointcut 接口中定義了兩個接口,分別用於返回類型過濾器和方法匹配器。下面我們再來看一下類型過濾器和方法匹配器接口的定義:

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 表達式對連接點進行選擇的能力。那下面我們不妨寫一個表達式對上一節的連接點進行選擇,比如下面這個表達式:

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)- 在目標方法調用前后均可執行自定義邏輯

上面是對通知的一些介紹,下面我們來看一下通知的源碼吧。如下:

public interface Advice {

}

如上,通知接口里好像什么都沒定義。不過別慌,我們再去到它的子類接口中一探究竟。

/** 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。我們先來看看這個接口的定義,如下:

public interface Advisor {

    Advice getAdvice();
    boolean isPerInstance();
}

public interface PointcutAdvisor extends Advisor {

    Pointcut getPointcut();
}

簡單來說一下 PointcutAdvisor 及其父接口 Advisor,Advisor 中有一個 getAdvice 方法,用於返回通知。PointcutAdvisor 在 Advisor 基礎上,新增了 getPointcut 方法,用於返回切點對象。因此 PointcutAdvisor 的實現類即可以返回切點,也可以返回通知,所以說 PointcutAdvisor 和切面的功能相似。不過他們之間還是有一些差異的,比如看下面的配置:

<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 源碼分析文章列表

Ⅰ. 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 原理探秘 - 容器的創建過程

本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處注明出處
作者:田小波
本文同步發布在我的個人博客:http://www.tianxiaobo.com

cc
本作品采用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM