SpringAop原始碼分析(基於註解)四:攔截器鏈
在之前的文章我們分析了通知器的建立與篩選和AOP建立代理物件的過程,現在代理物件已經有了,接下來我們看一下是如何執行通知器的邏輯的。
前言
通過閱讀這篇文章,可以瞭解到以下幾個問題:
- 通知的是如何起作用的?
- 多個通知的執行順序是怎樣的?
- 多個切面的多個通知的執行順序是怎樣的?
- @Transactional註解,呼叫本類方法為什麼不起作用?
下面我們可以帶著這些疑問來看
一、invoke()
本文依據JdkDynamicAopProxy
來分析,對CGLIB感興趣的同學看一看ObjenesisCglibAopProxy
相關程式碼。
JdkDynamicAopProxy實現了InvocationHandler
invoke()
方法:
//JdkDynamicAopProxy.java
public Object invoke(Object proxy,Method method,Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Object target = null;
try {
//如果目標物件沒有定義equals()方法的話,就會直接呼叫而不會增強
<1> if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
//如果目標物件沒有定義hashCode()方法的話,就會直接呼叫而不會增強
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised);
}
//Advised介面或者其父介面中定義的方法,直接反射呼叫,不應用通知
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised,method,args);
}
Object retVal;
// 如果 exposeProxy 屬性為 true,則暴露代理物件
// exposeProxy 是 @EnableAspectJAutoProxy 註解的屬性之一
<2> if (this.advised.exposeProxy) {
// Make invocation available if necessary.
// 向 AopContext 中設定代理物件
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
//獲得目標物件的類
<3> target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
//獲取可以應用到此方法上的 Interceptor 攔截器 列表,並且排序
<4> List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method,targetClass);
//如果沒有可以應用到此方法的通知(Interceptor),此直接反射呼叫 method.invoke(target,args)
<5> if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target,and no hot swapping or fancy proxying.
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method,args);
retVal = AopUtils.invokeJoinpointUsingReflection(target,argsToUse);
}
<6> else {
//建立 MethodInvocation,將攔截器鏈放入
invocation = new ReflectiveMethodInvocation(proxy,target,args,targetClass,chain);
// 執行攔截器鏈
retVal = invocation.proceed();
}
// 獲取方法返回值型別
<7> Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// 如果方法返回值為 this,即 return this; 則將代理物件 proxy 賦值給 retVal
retVal = proxy;
}
// 如果返回值型別為基礎型別,比如 int,long 等,當返回值為 null,丟擲異常
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
複製程式碼
這個invoke()方法主要有以下幾個步驟:
- <1>處,對
equals()、hashCode()
等方法進行判斷 - <2>處,處理
exposeProxy
屬性,代理的暴露方式 - <3>處,獲取目標物件的Class
- <4>處,獲取可以應用到當前方法的攔截器鏈
- <5>處,攔截器鏈為空,直接呼叫當前方法,不做增強
- <6>處,執行攔截器鏈
- <7>處,獲取返回值型別,並校驗
我們重點關注第<4>步和第<6>步,這兩個地方非常重要,第<2>步涉及比較多,最後我們再分析,先來看下第<4>步。
1.1、獲取可以應用到當前方法的攔截器鏈
程式碼:
//AdvisedSupport.java
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method,@Nullable Class<?> targetClass) {
MethodCacheKey cacheKey = new MethodCacheKey(method);
//從快取中獲取
List<Object> cached = this.methodCache.get(cacheKey);
if (cached == null) {
//獲取所有的攔截器
cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
this,targetClass);
this.methodCache.put(cacheKey,cached);
}
return cached;
}
複製程式碼
繼續深入:
//DefaultAdvisorChainFactory.java
/**
* 從提供的配置例項config中獲取advisor列表,遍歷處理這些advisor.如果是IntroductionAdvisor,* 則判斷此Advisor能否應用到目標類targetClass上.如果是PointcutAdvisor,則判斷
* 此Advisor能否應用到目標方法method上.將滿足條件的Advisor通過AdvisorAdaptor轉化成Interceptor列表返回.
*/
@Override
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
Advised config,@Nullable Class<?> targetClass) {
List<Object> interceptorList = new ArrayList<>(config.getAdvisors().length);
Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
//檢視是否包含IntroductionAdvisor
boolean hasIntroductions = hasMatchingIntroductions(config,actualClass);
//這裡實際上註冊一系列AdvisorAdapter,用於將 通知Advisor 轉化成 方法攔截器MethodInterceptor
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
//遍歷
for (Advisor advisor : config.getAdvisors()) {
if (advisor instanceof PointcutAdvisor) {
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
//通過ClassFilter對當前Bean型別進行匹配
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
//將 通知Advisor 轉化成 攔截器Interceptor
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
//檢查當前advisor的pointcut是否可以匹配當前方法
if (MethodMatchers.matches(mm,actualClass,hasIntroductions)) {
if (mm.isRuntime()) {
// Creating a new object instance in the getInterceptors() method
// isn't a problem as we normally cache created chains.
for (MethodInterceptor interceptor : interceptors) {
//加入攔截器鏈
interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor,mm));
}
}
else {
//加入攔截器鏈
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
}
else if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
// IntroductionAdvisor 型別的通知器,僅進行類級別的匹配即可
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
else {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
return interceptorList;
}
複製程式碼
這裡的主邏輯不是很複雜:
- 遍歷所有通知器
- 對於 PointcutAdvisor 型別的通知器,這裡要呼叫通知器所持有的切點(Pointcut)對類和方法進行匹配,匹配成功說明應向當前方法織入通知邏輯
- 將通知Advisor 轉化成 攔截器Interceptor
- 返回攔截器數鏈
這裡返回的攔截器鏈是有序的,按照afterReturn、after、around、before排好序,(具體排序是在獲取所有通知的時候sortAdvisors(eligibleAdvisors)
),方便後面執行。
1.2、執行攔截器鏈
現在有了攔截器鏈,接下來再看下怎麼執行的:
//ReflectiveMethodInvocation.java
//當前攔截器下標
private int currentInterceptorIndex = -1;
//攔截器鏈集合
protected final List<?> interceptorsAndDynamicMethodMatchers;
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
// 攔截器鏈中的最後一個攔截器執行完
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
//執行目標方法
return invokeJoinpoint();
}
//每次執行新的攔截器,下標+1
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
//如果要動態匹配切點
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
// 如果是動態切點,需要進行引數匹配,以確定是否需要執行該動態橫切邏輯
if (dm.methodMatcher.matches(this.method,this.targetClass,this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
//動態切點匹配失敗,略過當前Intercetpor,呼叫下一個Interceptor
return proceed();
}
}
else {
//執行當前Intercetpor
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
複製程式碼
proceed 根據currentInterceptorIndex
按照倒序執行攔截器鏈,每次執行完+1,當所有攔截器執行完後,再執行目標方法。
這裡我們可以提出幾個疑問:
- 當一方法匹配了多個通知時,不同通知之間是按照什麼順序來執行的?
- 從程式碼上看,所有通知都執行完畢後才會執行目標方法,那後置通知又是如何實現的?
我們帶著這2個疑問往下看,
proceed中的invoke(this)
方法根據通知的型別,有不同的實現類,如:
-
@Before 對應 MethodBeforeAdviceInterceptor
-
@After 對應 AspectJAfterAdvice
-
@AfterReturning 對應 AfterReturningAdviceInterceptor
-
@AfterThrowing 對應 AspectJAfterThrowingAdvice
-
@Around 對應 AspectJAroundAdvice
1.2.1、後置通知
由於攔截器鏈是按照倒序排列的,我們先來看下 後置通知 的程式碼:
//AspectJAfterAdvice.java
public class AspectJAfterAdvice extends AbstractAspectJAdvice
implements MethodInterceptor,AfterAdvice,Serializable {
public AspectJAfterAdvice(
Method aspectJBeforeAdviceMethod,AspectJExpressionPointcut pointcut,AspectInstanceFactory aif) {
super(aspectJBeforeAdviceMethod,pointcut,aif);
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
try {
//執行下一個攔截器鏈
return mi.proceed();
}
finally {
//執行後置邏輯
invokeAdviceMethod(getJoinPointMatch(),null,null);
}
}
@Override
public boolean isBeforeAdvice() {
return false;
}
@Override
public boolean isAfterAdvice() {
return true;
}
}
複製程式碼
可以看到,後置攔截器是會先繼續執行下一個攔截器,當攔截器鏈執行完畢之後,proceed()
再執行目標方法,最後執行後置邏輯。
1.2.2、環繞通知
我們再來看下環繞攔截器:
//AspectJAroundAdvice.java
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = getJoinPointMatch(pmi);
//執行環繞邏輯
return invokeAdviceMethod(pjp,jpm,null);
}
複製程式碼
環繞攔截器會直接執行環繞邏輯,而
由於我們之前在LogAspect
中配置瞭如下程式碼:
@Aspect
@Component
@EnableAspectJAutoProxy
public class LogAspect {
@Pointcut("execution(* com.mydemo.work.StudentController.getName(..))")
private void log(){}
@Before("log()")
public void doBefore() {
System.out.println("===before");
}
@After("execution(* com.mydemo.work.StudentController.getName(..))")
public void doAfter() {
System.out.println("===after");
}
@AfterReturning("execution(* com.mydemo.work.StudentController.getName(..))")
public void doAfterReturn() {
System.out.println("===afterReturn");
}
@Around("execution(* com.mydemo.work.StudentController.getName(..))")
public void doAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("===around before");
pjp.proceed();
System.out.println("===around after");
}
}
複製程式碼
所以該攔截器會先執行========around before
,然後再執行下一個攔截器。
1.2.3、前置通知
我們再來看下前置攔截器:
//MethodBeforeAdviceInterceptor.java
public class MethodBeforeAdviceInterceptor implements MethodInterceptor,Serializable {
private MethodBeforeAdvice advice;
/**
* Create a new MethodBeforeAdviceInterceptor for the given advice.
* @param advice the MethodBeforeAdvice to wrap
*/
public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
Assert.notNull(advice,"Advice must not be null");
this.advice = advice;
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
//執行前置邏輯
this.advice.before(mi.getMethod(),mi.getArguments(),mi.getThis() );
//執行下一個攔截器
return mi.proceed();
}
}
複製程式碼
可以看到前置攔截器在執行完 前置方法 之後,又呼叫MethodInvocation#proceed()
方法,繼續去執行下一個攔截器。
二、通知的執行順序
2.1、同一Aspect下
到這裡大家可能有點繞,我們來捋一下通知的執行順序,也就是攔截器的執行順序。
首先,前面說過攔截器鏈是按照倒序排序的,如下:
畫一下執行流程圖: 再用單元測試跑一遍
LogAspect
的相關程式碼,列印結果如下:
===around before
===before
do getName
===around after
===after
===afterReturn
複製程式碼
和我們猜測的一樣。現在我們可以回答前面的問題了,就是一個方法匹配多個通知時,按照什麼順序執行。
2.2、多個Aspect下
但是,在實際中,通常一個方法可能被多個Aspect攔截,比如我們想讓業務方法先被 日誌Aspect 攔截,然後被 異常Aspect攔截。
Spring框架已經替我們想到了這個問題,如果我們有多個Aspect,實際上它們會隨機執行,沒有明確的順序。但是Spring提供了@Order
註解,可以讓我們指定Aspect的執行順序。
比如我們新加一個處理異常的Aspect:
@Aspect
@Component
@EnableAspectJAutoProxy
@Order(2)
public class ErrorAspect {
@Pointcut("execution(* com.mydemo.work.StudentController.getName(..))")
private void log(){}
@Before("log()")
public void doBeforeError() {
System.out.println("=== error before");
}
@After("execution(* com.mydemo.work.StudentController.getName(..))")
public void doAfterError() {
System.out.println("=== error after");
}
@AfterReturning("execution(* com.mydemo.work.StudentController.getName(..))")
public void doAfterReturnError() {
System.out.println("=== error afterReturn");
}
@Around("execution(* com.mydemo.work.StudentController.getName(..))")
public void doAroundError(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("=== error around before");
pjp.proceed();
System.out.println("=== error around after");
}
}
複製程式碼
同時給LogAspect
加上註解@Order(1)
,也就意味著LogAspect
的通知要比ErrorAspect
先執行。
先來單元測試跑一下看看結果:
===around before
===before
=== error around before
=== error before
do getName
=== error around after
=== error after
=== error afterReturn
===around after
===after
===afterReturn
複製程式碼
可以看到,確實是LogAspect
的通知先執行了,這是為什麼呢?我們來debug原始碼看一下,主要看攔截器鏈,如圖:
可以看到,攔截器鏈已經按照順序排列好了,所以程式碼只要按照這個攔截器鏈順序執行,就能保證2個切面有序攔截。
那麼它為什麼要這樣排序呢?我們來畫個圖:
如上圖所示,這2個Aspect就像2個圓圈在外面攔截,中間是目標方法。
當一個請求進來要執行目標方法:
- 首先會被外圈的
@Order(1)
攔截器攔截 - 然後被內圈
@Order(2)
攔截器攔截 - 執行完目標方法後,先經過
@Order(2)
的後置攔截器 - 最後再通過
@Order(1)
的後置攔截器
到這裡我們多個通知的執行順序就分析完了,這裡面還是要去從原始碼層面理解,不能死記硬背,這樣才能記得牢固。
三、exposeProxy
上面我們分析invoke()
方法的時候,有下面這段程式碼:
// 如果 exposeProxy 屬性為 true,則暴露代理物件
// exposeProxy 是 @EnableAspectJAutoProxy 註解的屬性之一
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
// 向 AopContext 中設定代理物件
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
複製程式碼
本小節我們就來詳細分析一下這裡是什麼意思。
首先 exposeProxy 是註解@EnableAspectJAutoProxy
中的一個屬性,它可以被設定為true或false,預設是false。
這個屬性是為了解決目標方法呼叫同物件中其他方法時,其他方法的切面邏輯無法執行的問題。
啥意思呢?我們來舉個例子:
public class Student implements Person {
@Override
public void haveFun() {
System.out.println("籃球");
this.hello("足球");
}
@Override
public void haveFun(String action) {
System.out.println("haveFun " + action);
}
}
複製程式碼
在同一個類Student.class
中,一個haveFun()
方法呼叫了本類中的另一個haveFun(String action)
方法,此時haveFun(String action)
上的切面邏輯就無法執行了。
3.1、為什麼本類呼叫無效?
那為什麼會這樣呢?
我們知道AOP的本質就是給目標物件生成一個代理物件,對原本的方法進行增強,之後執行的方法都是我們代理類中的方法。
而haveFun()
方法中使用的是this.hello("足球")
,這裡的this不是代理物件,而是原始物件,因此通過原始物件呼叫haveFun(String action)
是不會被增強的,所以切面就不會起作用。
那麼問題又來了,為什麼這裡的this不是代理物件,而是原始物件呢?
3.2、為什麼this不是代理物件
上面程式碼中有一個ReflectiveMethodInvocation#proceed()
方法如下:
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
// 攔截器鏈中的最後一個攔截器執行完
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
//執行目標方法
return invokeJoinpoint();
}
//...省略
if (dm.methodMatcher.matches(this.method,this.arguments)) {
return dm.interceptor.invoke(this);
}
//...省略
}
else {
//執行當前Intercetpor
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
複製程式碼
在所有攔截器執行完之後,會去執行目標方法,我們跟蹤這個方法invokeJoinpoint()
//ReflectiveMethodInvocation.java
protected Object invokeJoinpoint() throws Throwable {
//重點 this.target
return AopUtils.invokeJoinpointUsingReflection(this.target,this.method,this.arguments);
}
複製程式碼
//ReflectiveMethodInvocation.java
public static Object invokeJoinpointUsingReflection(@Nullable Object target,Object[] args)
throws Throwable {
// Use reflection to invoke the method.
try {
ReflectionUtils.makeAccessible(method);
//重點 target
return method.invoke(target,args);
}
//...省略
}
複製程式碼
可以看到,這裡呼叫的是原始目標物件target
來執行我們的目標方法,所以此時haveFun()
方法中的this.hello("足球")
的這個this其實是原始目標物件。
3.3、exposeProxy是如何起作用的?
所以exposeProxy
這個屬性就是來解決這個問題的,那麼它是如何起作用的呢?
我們回過頭來看程式碼:
// 如果 exposeProxy 屬性為 true,則暴露代理物件
// exposeProxy 是 @EnableAspectJAutoProxy 註解的屬性之一
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
// 向 AopContext 中設定代理物件
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
複製程式碼
繼續跟蹤:
//AopContext.java
//儲存代理物件 ThreadLocal
private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");
static Object setCurrentProxy(@Nullable Object proxy) {
Object old = currentProxy.get();
if (proxy != null) {
//儲存代理物件到 ThreadLocal
currentProxy.set(proxy);
}
else {
currentProxy.remove();
}
return old;
}
複製程式碼
如果exposeProxy
被設定為了true,會把代理物件存到ThreadLocal
中,而在本類中呼叫的時候,會從ThreadLocal
中獲取代理類來呼叫目標方法,就可以解決本來呼叫的問題。
最後再來看下面這種情況:
public class Student implements Person {
@Override
@Transactional
public void haveFun() {
System.out.println("籃球");
this.hello("足球");
}
@Override
@Transactional
public void haveFun(String action) {
System.out.println("haveFun " + action);
}
}
複製程式碼
這種情況下,haveFun(String action)
的事務不會生效,原因就是我們剛才分析的。實際上@Transactional
本質上也是AOP實現的,所以也會有這個問題。
解決方案就是給@EnableAspectJAutoProxy
註解設定exposeProxy=true
總結
時序圖:
到這裡SpringAOP的原始碼分析就告一段落了,由於本人經驗和技術水平有限,所以只能先了解這麼多,如有錯誤,歡迎提出意見。