理解Spring(二):AOP 的概念與實現原理

  • 2020 年 7 月 23 日
  • 筆記

什麼是 AOP

AOP(Aspect Oriented Programming,面向切面編程)是一種編程範式,它是對 OOP(Object Oriented Programming,面向對象編程)的一個補充。

OOP 允許我們通過類來定義對象的屬性和行為,由於對象的行為是通過類中的方法來體現的,所以要想修改一個對象的行為,就必須修改類中相應的方法。試想這麼一個場景,我們需要對某些對象的某些行為進行耗時統計,OOP 的做法只能是挨個去修改它們所屬的類,在相應的方法上加入耗時統計的邏輯,如果只是針對少量幾個行為的修改倒也無妨,但如果要統計的是成百上千個行為呢,挨個去修改這成百上千個方法就顯得很拙劣,而且還會導致大量的代碼重複,如果要統計的是第三方類庫中的行為,那麼 OOP 就顯得更加力不從心了。

在實際開發中,除耗時統計之外,類似的還有日誌記錄、事務控制、權限驗證等等,它們往往穿插在各個控制流中,被各個功能模塊所調用,但它們卻是與核心業務邏輯無關的。像這種穿插在各個功能模塊中的且與核心業務無關的代碼被稱為橫切(cross cutting)。

在傳統 OOP 中,橫切除了會導致大量的代碼重複之外,還會使核心業務代碼看起來臃腫,由於那些與核心業務無關的橫切代碼同核心業務代碼緊密耦合在一起,甚至會出現核心業務代碼被淹沒在大量橫切代碼之中的情況,而且這些橫切代碼分散在系統的各個地方,非常不利於維護和管理。

AOP 提供了對橫切的處理思路,它的主要思想是,將橫切邏輯分離出來,封裝成切面,通過某種機制將其織入到指定的各個功能模塊中去,而不再是同核心業務代碼交織在一起。AOP 使得我們可以暫時忽略掉系統中的橫切邏輯,專註於核心業務邏輯的開發,實現橫切邏輯與核心業務邏輯的解耦,允許我們對橫切代碼進行集中管理,消除代碼重複。

AOP 的基本術語

切面(Aspect):是對橫切邏輯的抽象,一個切面由通知和切點兩部分組成。在實際應用中,切面被定義成一個類。

通知(Advice):是橫切邏輯的具體實現。在實際應用中,通知被定義成切面類中的一個方法,方法體內的代碼就是橫切代碼。通知的分類:以目標方法為參照點,根據切入方位的不同,可分為前置通知(Before)、後置通知(AfterReturning)、異常通知(AfterThrowing)、最終通知(After)與環繞通知(Around)5種。

切點(Pointcut):用於說明將通知織入到哪個方法上,它是由切點表達式來定義的。

目標對象(Target):是指那些即將織入切面的對象。這些對象中已經只剩下乾乾淨淨的核心業務邏輯的代碼了,所有的橫切邏輯的代碼都等待 AOP 框架的織入。

代理對象(Proxy):是指將切面應用到目標對象之後由 AOP 框架所創建的對象。可以簡單地理解為,代理對象的功能等於目標對象的核心業務邏輯功能加上橫切邏輯功能,代理對象對使用者而言是透明的。

織入(Weaving):是指將切面應用到目標對象從而創建一個新的代理對象的過程。

Spring AOP 的簡單應用

Spring 的 AOP 模塊簡稱 Spring AOP,該模塊對 AOP 提供了支持。

使用 Spring 進行面向切面編程的基本步驟如下:

一、定義一個切面。使用 @Aspect 註解聲明切面,並使用 @Component 註解將該 Bean 註冊到 Spring 容器。

@Aspect
@Component
public class WebLogAspect {}

二、在切面中定義一個切點。通過 @Pointcut 註解指定切點表達式。

@Pointcut("execution(public * com.example.demo.controller.*.*(..))")
public void controllerLog(){}

三、在切面中定義一個通知。例如使用 @Before 註解定義一個前置通知,並為其指定一個切點。然後在通知的方法體內編寫橫切代碼。

@Before("controllerLog()")
public void beforeAdvice(JoinPoint joinPoint){
    logger.info("前置通知...");
}

以上是基於註解的切面定義方式,我們會發現這些註解是由 AspectJ 提供的。AspectJ 是一個專門的 AOP 框架,它提供了比 Spring AOP 更為強大的功能。那 Spring AOP 與 AspectJ 有什麼關係呢?其實沒有什麼關係,只不過是 Spring AOP 把 AspectJ 的註解直接拿來用了罷了。所以上面這種基於註解的方式也被稱為 AspectJ 風格。

採用 AspectJ 風格來定義切面,需要開啟 AspectJ 自動代理選項,如使用註解 @EnableAspectJAutoProxy 或配置 XML 標籤 <aop:aspectj-autoproxy>

根據我們上面配置的切點表達式,Spring 會給 com.example.demo.controller 包下的所有的類都生成相應的代理類,並將橫切代碼 logger.info("前置通知..."); 織入到代理類的每一個 public 方法中,由於我們定義的是前置通知,所以它會被織入到方法內其他代碼的前面。然後 Spring 會生成代理類的實例作為代理對象,並將其加入到 Spring 容器的單例池中。當我們拿到代理對象之後,調用它們的 public 方法首先執行的是 logger.info("前置通知..."); 這行橫切代碼,然後才是我們在目標類中寫的代碼。當然,如果我們定義是後置通知(AfterReturning),那麼與前置通知剛好相反,這行橫切代碼會被織入到方法內其他代碼的後面。

通過 AOP,我們將橫切代碼與核心業務代碼進行了分離,然後又通過某種機制將其聯繫了起來,在 Spring AOP 中,這個機制就是動態代理。

Spring AOP 與動態代理

Spring AOP 是基於動態代理技術來實現的,因此需要了解什麼是動態代理。

動態代理是代理模式的一種實現方式,我們先來看一下什麼是代理模式。

代理模式是 GoF 的 23 種設計模式之一,代理模式允許我們在不修改目標類的前提下對目標對象的行為做一些補充。它是通過在客戶端對象與目標對象之間引入一個代理對象來實現的,代理對象相當於一個中介,負責代理目標對象的業務,並且它在代理業務的同時還可以添油加醋,有了代理對象之後,客戶端對象訪問代理對象,既能實現目標業務,而且還能讓代理對象在目標業務的基礎上增加一些額外的服務,如「端茶送水」等,當然代理對象可能需要「收點小費」了。如果沒有代理對象,客戶端對象就享受不到「端茶送水」的額外服務,除非修改目標對象的行為。

代理模式分為靜態代理與動態代理。

靜態代理需要我們手動編寫代理類,代理類需要實現與目標類相同的接口,並通過構造方法傳入目標對象,然後調用目標對象的相應方法,將具體業務委託給目標對象來執行,並在委託時可以做一些處理。由於靜態代理需要我們手動編寫代理類,大大增加了我們的工作量,並且還可能導致大量的代碼重複,因此,自 JDK1.3 引入了動態代理技術之後,我們更加偏向使用動態代理。

動態代理基於反射技術,允許程序在運行期間動態生成代理類與代理對象,這樣就不需要我們編寫代理類了。

動態代理有兩種實現方式,一種是基於 JDK 的動態代理,另一種是基於 CGLib 的動態代理,也就是說,一個是使用 JDK 提供的動態代理技術來實現,一個是使用第三方庫 CGLib 提供的動態代理技術來實現。

基於 JDK 的動態代理是面向接口的代理,它要求目標類必須實現至少一個接口,其動態生成的代理類也會實現同樣的接口。基於 CGLib 的動態代理是面向類的代理,它所生成的代理類是目標類的一個子類,因此要求目標類和目標方法不能聲明為 final。

Spring AOP 通過 JDK 或 CGLib 的動態代理技術,將橫切代碼動態織入到目標類的方法前後,並生成一個代理對象,用這個織入了橫切邏輯後的代理對象充當目標對象供我們使用。

Spring AOP 的實現原理(源碼分析)

我們知道,當一個 Bean 被實例化出來之後,Spring 會對其執行一些初始化操作,如:回調 Aware 接口方法、調用 init 方法、應用後置處理器等。其中應用後置處理器的代碼如圖所示。

該方法會遍歷所有已註冊的 Bean 後置處理器,依次調用它們的 postProcessAfterInitialization() 方法對 Bean 實例執行相應的處理。我們在這個地方打個斷點,看看它都註冊了哪些後置處理器。

在這些 Bean 後置處理器當中,有一個 AnnotationAwareAspectJAutoProxyCreator 對象,顧名思義,它是一個基於註解的「代理創建器」。我們猜測,代理類的創建就是在這個後置處理器中進行的。它的postProcessAfterInitialization() 方法如圖所示。

這個名為「代理創建器」的後置處理器主要做的事情就是調用 wrapIfNecessary() 方法。該方法的具體實現如圖所示。

我們在 wrapIfNecessary() 方法中發現了創建代理的邏輯,看來一切要真相大白了。該方法會根據需要,為給定的 Bean 實例(即目標對象)創建代理並返回代理對象,或者將該 Bean 實例原封不動直接返回。

至此可以得知,代理對象的創建是在 Bean 的初始化階段完成的,是通過名為「代理創建器」的這麼一個後置處理器來實現的。

我們進入到 createProxy() 方法中,看一下創建代理的具體實現。

該方法主要是創建並配置 ProxyFactory 對象(如配置 Advisor 、設置目標對象等),然後調用它的 getProxy() 方法得到一個代理對象。

這裡順便介紹一下 Advisor。在 Spring 內部,每個切面都會被封裝成一個 Advisor 對象,一個 Advisor 對象內部包含一個通知對象( Advice )和一個切點對象(Pointcut),因此可以說,Advisor 對象就是真正的切面對象。

上面的 getProxy() 方法先是會調用 createAopProxy() 方法創建一個 AopProxy 對象,然後將創建代理的任務委託給 AopProxy 對象來執行。AopProxy 本身是一個接口,它主要有兩個實現類:一個是 JdkDynamicAopProxy,一個是 ObjenesisCglibAopProxy。顧名思義,前者使用 JDK 動態代理技術,後者使用 CGLib 動態代理技術。 createAopProxy() 方法會根據條件選擇使用哪種動態代理技術,具體實現如圖所示。

大體來說,在默認情況下,如果目標類沒有實現任何接口,那麼就使用 CGLib 動態代理,否則使用 JDK 動態代理。由於 CGLib 的性能相對較好,我們可以通過開啟 proxyTargetClass 選項強制 Spring 始終使用 CGLib 動態代理。(註:Spring Boot 默認開啟了 proxyTargetClass

AopProxy 的功能很簡單,就是使用動態代理技術生成代理類及其實例,JdkDynamicAopProxy 通過 JDK 提供的 ProxyInvocationHandler 來實現,ObjenesisCglibAopProxy 通過 CGLib 提供的 Enhancer 來實現。(註:Spring AOP 中集成並定製了 CGLib,因此無需引入外部的 CGLib 依賴)。

總結:Spring AOP 的核心是「代理創建器」,也就是 AbstractAutoProxyCreator 的子類,本質上它是一個 Bean 的後置處理器,Spring 會根據我們的配置,將相應的「代理創建器」註冊到 Spring 容器,例如當我們項目中配置了 @EnableAspectJAutoProxy 註解時,Spring 就會將 AnnotationAwareAspectJAutoProxyCreator 註冊到 Spring 容器。由於它是一個 Bean 的後置處理器,所以它會在 Bean 的初始化階段得到調用,它會首先判斷當前這個 Bean 是否需要被代理,如果不需要,直接將原 Bean 實例返回,如果需要,就使用動態代理技術為當前 Bean 創建一個代理類,並將橫切代碼織入到代理類中,然後生成一個代理類的實例並將其返回,也就是用代理對象充當 Bean 實例。如果該 Bean 是單例的,那麼這個代理對象就會被加入到 Spring 容器的單例池中,之後當我們 getBean 時,就可以直接從單例池中拿到這個代理對象。

擴展:為什麼 JDK 動態代理要求目標類必須實現接口

通過查看 java.lang.reflect.Proxysun.misc.ProxyGenerator 的源碼,不難發現, 它所生成的代理類都繼承自 java.lang.reflect.Proxy。關鍵代碼如圖所示。


由於 Java 不支持多繼承,所以既然代理類繼承了 Proxy ,那麼就無法再繼承目標類了,但是代理類與目標類之間必須要建立一種關係,以保證代理對象能夠被引用到,且對使用者而言是透明的,這樣就只能通過接口來實現了,也就是讓代理類實現與目標類相同的接口,用接口類型的變量去接收代理類的實例。