1. 程式人生 > >Spring5原始碼分析系列(十)SpringAOP設計原理及應用場景

Spring5原始碼分析系列(十)SpringAOP設計原理及應用場景

本章開始講解SpringAOP設計原理及應用場景,文章參考自Tom老師視訊~~

SpringAOP應用示例

AOP是OOP的延續,是AspectOrientedProgramming的縮寫,意思是面向切面程式設計。可以通過預編譯方式和執行期動態代理實現在不修改原始碼的情況下給程式動態統一新增功能的一種技術。AOP設計模式孜孜不倦追求的是呼叫者和被呼叫者之間的解耦,AOP可以說也是這種目標的一種實現。

我們現在做的一些非業務,如:日誌、事務、安全等都會寫在業務程式碼中(也即是說,這些非業務類橫切於業務類),但這些程式碼往往是重複,複製——貼上式的程式碼會給程式的維護帶來不便,AOP就實現了把這些業務需求與系統需求分開來做。這種解決的方式也稱代理機制。

先來了解一下AOP的相關概念

切面(Aspect):官方的抽象定義為“一個關注點的模組化,這個關注點可能會橫切多個物件”。“切面”在ApplicationContext中<aop:aspect>來配置。

連線點(Joinpoint):程式執行過程中的某一行為,例如,MemberService.get的呼叫或者MemberService.delete丟擲異常等行為。

通知(Advice):“切面”對於某個“連線點”所產生的動作。其中,一個“切面”可以包含多個“Advice”。

切入點(Pointcut):匹配連線點的斷言,在AOP中通知和一個切入點表示式關聯。切面中的所有通知所關注的連線點,都由切入點表示式來決定。

目標物件(TargetObject):被一個或者多個切面所通知的物件。例如,AServcieImpl和BServiceImpl,當然在實際執行時,SpringAOP採用代理實現,實際AOP操作的是TargetObject的代理物件。

AOP代理(AOPProxy):在SpringAOP中有兩種代理方式,JDK動態代理和CGLIB代理。預設情況下,TargetObject實現了介面時,則採用JDK動態代理,例如,AServiceImpl;反之,採用CGLIB代理,例如,BServiceImpl。強制使用CGLIB代理需要將<aop:config>的proxy-target-class屬性設為true。

通知(Advice)型別:前置通知(Beforeadvice):在某連線點(JoinPoint)之前執行的通知,但這個通知不能阻止連線點前的執行。ApplicationContext中在<aop:aspect>裡面使用<aop:before>元素進行宣告。例如,TestAspect中的doBefore方法。

後置通知(Afteradvice):當某連線點退出的時候執行的通知(不論是正常返回還是異常退出)。ApplicationContext中在<aop:aspect>裡面使用<aop:after>元素進行宣告。例如,ServiceAspect中的returnAfter方法,所以Teser中呼叫UserService.delete丟擲異常時,returnAfter方法仍然執行。

返回後通知(Afterreturnadvice):在某連線點正常完成後執行的通知,不包括丟擲異常的情況。ApplicationContext中在<aop:aspect>裡面使用<after-returning>元素進行宣告。

環繞通知(Aroundadvice):包圍一個連線點的通知,類似Web中Servlet規範中的Filter的doFilter方法。可以在方法的呼叫前後完成自定義的行為,也可以選擇不執行。ApplicationContext中在<aop:aspect>裡面使用<aop:around>元素進行宣告。例如,ServiceAspect中的around方法。丟擲異常後通知(Afterthrowingadvice):在方法丟擲異常退出時執行的通知。ApplicationContext中在<aop:aspect>裡面使用<aop:after-throwing>元素進行宣告。例如,ServiceAspect中的returnThrow方法。

注:可以將多個通知應用到一個目標物件上,即可以將多個切面織入到同一目標物件。

使用SpringAOP可以基於兩種方式,一種是比較方便和強大的註解方式,另一種則是中規中矩的xml配置方式。

先說註解,使用註解配置SpringAOP總體分為兩步,第一步是在xml檔案中宣告啟用自動掃描元件功能,同時啟用自動代理功能(來測試AOP的註解功能):

第二步是為Aspect切面類添加註解:

測試程式碼

控制檯輸出如下:

可以看到,正如我們預期的那樣,雖然我們並沒有對MemberService類包括其呼叫方式做任何改變,但是Spring仍然攔截到了其中方法的呼叫,或許這正是AOP的魔力所在。

再簡單說一下xml配置方式,其實也一樣簡單:

個人覺得不如註解靈活和強大,你可以不同意這個觀點,但是不知道如下的程式碼會不會讓你的想法有所改善:以下是MemberService的程式碼:

應該說學習SpringAOP有兩個難點,第一點在於理解AOP的理念和相關概念,第二點在於靈活掌握和使用切入點表示式。概念的理解通常不在一朝一夕,慢慢浸泡的時間長了,自然就明白了,下面我們簡單地介紹一下切入點表示式的配置規則吧。

通常情況下,表示式中使用”execution“就可以滿足大部分的要求。表示式格式如下:

modifiers-pattern:方法的操作許可權

ret-type-pattern:返回值

declaring-type-pattern:方法所在的包

name-pattern:方法名

parm-pattern:引數名

throws-pattern:異常

其中,除ret-type-pattern和name-pattern之外,其他都是可選的。上例中,execution(*com.spring.service.*.*(..))表示com.spring.service包下,返回值為任意型別;方法名任意;引數不作限制的所有方法。

最後說一下通知引數

可以通過args來繫結引數,這樣就可以在通知(Advice)中訪問具體引數了。例如,<aop:aspect>配置如下:

上面的程式碼args(msg,..)是指將切入點方法上的第一個String型別引數新增到引數名為msg的通知的入參上,這樣就可以直接使用該引數啦。

訪問當前的連線點

在上面的Aspect切面Bean中已經看到了,每個通知方法第一個引數都是JoinPoint。其實,在Spring中,任何通知(Advice)方法都可以將第一個引數定義為org.aspectj.lang.JoinPoint型別用以接受當前連線點物件。JoinPoint介面提供了一系列有用的方法,比如getArgs()(返回方法引數)、getThis()(返回代理物件)、getTarget()(返回目標)、getSignature()(返回正在被通知的方法相關資訊)和toString()(打印出正在被通知的方法的有用資訊)。

SpringAOP設計原理及原始碼分析

開始之前先上圖,看看Spring中主要的AOP元件

Spring提供了兩種方式來生成代理物件:JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport物件的配置來決定。預設的策略是如果目標類是介面,則使用JDK動態代理技術,否則使用Cglib來生成代理。下面我們來研究一下Spring如何使用JDK來生成代理物件,具體的生成程式碼放在JdkDynamicAopProxy這個類中,直接上相關程式碼:

那這個其實很明瞭,註釋上我也已經寫清楚了,不再贅述。

下面的問題是,代理物件生成了,那切面是如何織入的?

我們知道InvocationHandler是JDK動態代理的核心,生成的代理物件的方法呼叫都會委託到InvocationHandler.invoke()方法。而通過JdkDynamicAopProxy的簽名我們可以看到這個類其實也實現了InvocationHandler,下面我們就通過分析這個類中實現的invoke()方法來具體看下SpringAOP是如何織入切面的。

主流程可以簡述為:獲取可以應用到此方法上的通知鏈(InterceptorChain),如果有,則應用通知,並執行joinpoint;如果沒有,則直接反射執行joinpoint。而這裡的關鍵是通知鏈是如何獲取的以及它又是如何執行的,下面逐一分析下。

首先,從上面的程式碼可以看到,通知鏈是通過Advised.getInterceptorsAndDynamicInterceptionAdvice()這個方法來獲取的,我們來看下這個方法的實現:

可以看到實際的獲取工作其實是由AdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice()這個方法來完成的,獲取到的結果會被快取。下面來分析下這個方法的實現:

這個方法執行完成後,Advised中配置能夠應用到連線點或者目標類的Advisor全部被轉化成了MethodInterceptor.

接下來我們再看下得到的攔截器鏈是怎麼起作用的。

從這段程式碼可以看出,如果得到的攔截器鏈為空,則直接反射呼叫目標方法,否則建立MethodInvocation,呼叫其proceed方法,觸發攔截器鏈的執行,具體程式碼如下: