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

張永恆發表於2020-07-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 ,那麼就無法再繼承目標類了,但是代理類與目標類之間必須要建立一種關係,以保證代理物件能夠被引用到,且對使用者而言是透明的,這樣就只能通過介面來實現了,也就是讓代理類實現與目標類相同的介面,用介面型別的變數去接收代理類的例項。

相關文章