Spring5.X AOP 通知的遞歸調用鏈實現源碼分析

寫在前面

  • 具備Spring IOC以及AOP源碼分析基礎 <單擊一鍵,原理盡顯>
  • Advisor
      它包含AOP 通知(在連接點採取的操作)和確定建議適用性的過濾器(例如切入點),簡單的說的來說可以理解成攔截器或者切面。
  • @DeclareParents
       用來標註對象屬性,可以爲對象添加一個新方法。
  • InterceptorAndDynamicMethodMatcher
       動態攔截器,根據運行時參數來決定攔截器是否生效,有動態攔截器當然也有靜態攔截器,靜態攔截器一般通過包名,類名,方法名 ,參數來確定靜態攔截器是否對代理方法生效。
  • 本文調試代碼獲取
  • 閱讀本文你將收穫
    1. AOP切面配置如何被加載的?
    2. PartialOrder究竟對Advice(MethodIntercept)做出了何種排序?
    3. AOP如何通過遞歸實現的調用鏈設計模式?
    4. 事務爲何會失效?

前情回顧

  AOP的一切要從 @EnableAspectJAutoProxy說起。

  它通過 @Import註解AspectJAutoProxyRegistrar類,這個類是ImportBeanDefinitionRegistrar實現類。

  通過這種手動注入方式,最終將AnnotationAwareAspectJAutoProxyCreator注入到IOC容器中。

  查看AnnotationAwareAspectJAutoProxyCreator的類圖,可以發現它是BeanPostProcessor的實現,BeanPostProcessor允許對Bean進行增強操作。

  增強操作分爲兩種:前置(postProcessBeforeInitialization)、後置(postProcessAfterInitialization)增強,最終我們在AnnotationAwareAspectJAutoProxyCreator的其中一個超類中找到了這兩個增強方法的實現,那就是AbstractAutoProxyCreator

	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean;
	}

	/**
	 * Create a proxy with the configured interceptors if the bean is
	 * identified as one to proxy by the subclass.
	 * @see #getAdvicesAndAdvisorsForBean
	 */
	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

  最終,我們發現AbstractAutoProxyCreator在前置增強中並未做任何處理,而我們的故事也就是從後置增強中開始……

  咳咳~

  好啦,故事我們講完了,現在開始源碼分析。

源碼分析

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		// 省略部分代碼……
		// 1、獲取所有Advisors(可以理解成攔截器或者切面),官方定義https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/aop/Advisor.html
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
		if (specificInterceptors != DO_NOT_PROXY) {
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
			// 2、創建代理
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}

  我將整個過程分兩步:

            1、切入點的解析與Advice初排序
            2、代理中的遞歸調用鏈邏輯分析

  它們分別隸屬getAdvicesAndAdvisorsForBean()分支以及createProxy()分支,那麼我們先來看第一部分的源碼分析。

切入點的解析與Advice初排序

切入點加載的調用鏈源碼解析

  IDEA Ctrl+Alt+B 可以發現,getAdvicesAndAdvisorsForBean有兩種實現,當方法是具有多個實現的抽象方法時,可以選擇調試的方式確定最終的調用方向。

  最終,我們可以確定代碼走向AbstractAdvisorAutoProxyCreator的實現。

	protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
		List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
		if (advisors.isEmpty()) {
			return DO_NOT_PROXY;
		}
		return advisors.toArray();
	}
	
	// 尋找合格的顧問
——> protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
	// 查詢所有候選Advisors
	// Ctrl+Alt+B 進入AnnotationAwareAspectJAutoProxyCreator的findCandidateAdvisors()實現
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}
	

——> protected List<Advisor> findCandidateAdvisors() {
		// 執行父類中的findCandidateAdvisors()方法,也就是我們上一段代碼所在類。
		List<Advisor> advisors = super.findCandidateAdvisors();
		// 查詢添加Bean工廠中所有的AspectJ切面Advisor。
		if (this.aspectJAdvisorsBuilder != null) {
		// 查詢添加Bean工廠中所有的AspectJ切面Advisor,被@AspectJ標註的類將被IOC做特殊標記
			advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
		}
		return advisors;
	}

  到這裏我們可以初略的將Advisor分爲兩類,過濾器以及切面,接下來我們重點關注一下切面(@AspectJ標註的類)如何被解析的?以及如何進行的排序?

——> public List<Advisor> buildAspectJAdvisors() {
		List<String> aspectNames = this.aspectBeanNames;

		if (aspectNames == null) {
			synchronized (this) {
				aspectNames = this.aspectBeanNames;
				if (aspectNames == null) {
					List<Advisor> advisors = new LinkedList<>();
					aspectNames = new LinkedList<>();
					String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
							this.beanFactory, Object.class, true, false);
					for (String beanName : beanNames) {
						if (!isEligibleBean(beanName)) {
							continue;
						}
						// We must be careful not to instantiate beans eagerly as in this case they
						// would be cached by the Spring container but would not have been weaved.
						Class<?> beanType = this.beanFactory.getType(beanName);
						if (beanType == null) {
							continue;
						}
						// 1、過濾被@AspectJ標註的Bean
						if (this.advisorFactory.isAspect(beanType)) {
							aspectNames.add(beanName);
							AspectMetadata amd = new AspectMetadata(beanType, beanName);
							if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
								MetadataAwareAspectInstanceFactory factory =
										new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
								// 2、獲取當前切面下的所有Advisor切入點
								List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
								if (this.beanFactory.isSingleton(beanName)) {
									this.advisorsCache.put(beanName, classAdvisors);
								}
								else {
									this.aspectFactoryCache.put(beanName, factory);
								}
								advisors.addAll(classAdvisors);
							}
							else {
								// 省略部分代碼……
							}
						}
					}
					this.aspectBeanNames = aspectNames;
					return advisors;
				}
			}
		}

		// 省略部分尾部代碼……
		return advisors;
	}

  Spring 如何獲取切入點信息?初排序Order如何設置?

——>public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
		Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
		String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
		// 驗證當前實例,驗證@AspectJ標註等
		validate(aspectClass);

		// 延遲加載相關.
		MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
				new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

		List<Advisor> advisors = new LinkedList<>();
		for (Method method : getAdvisorMethods(aspectClass)) {
		// 我們重點關注這一行,可以看到的是
		// declarationOrderInAspect參數入參爲 advisors.size(),這意味着
		// 這與Advisor之後的Order排序有着密切的關係,從賦值來看,順序爲Advisor聲明的順序
			Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

		// If it's a per target aspect, emit the dummy instantiating aspect.
		if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
			Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
			advisors.add(0, instantiationAdvisor);
		}

		// 檢查是否又被@DeclareParents標註的屬性.
		for (Field field : aspectClass.getDeclaredFields()) {
			Advisor advisor = getDeclareParentsAdvisor(field);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

		return advisors;
	}

  @DeclareParents用來標註對象屬性,可以爲對象添加一個新方法。繼續追蹤調用鏈,查看如何創建的切入點Advisor:

——>	public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
			int declarationOrderInAspect, String aspectName) {
		// 1、超類@AspectJ標註等驗證
		validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());
		// 2、獲取切入點配置信息。說白了,就是在查找方法上是否有AOP切入點相關的註解標註
		AspectJExpressionPointcut expressionPointcut = getPointcut(
				candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
		// 在這裏將過濾掉所有沒有切入點配置的方法
		if (expressionPointcut == null) {
			return null;
		}
		// 3、構造切入點Advisor,關注到declarationOrderInAspect的賦值
		return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
				this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
	}

	// 看看Spring是如何獲取到方法上面的切入點的
——>	private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
		// 查找方法是否有被@AspectJ類註解標註
		AspectJAnnotation<?> aspectJAnnotation =
				AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
		if (aspectJAnnotation == null) {
			return null;
		}
		// 構造成切入點對象
		AspectJExpressionPointcut ajexp =
				new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]);
		ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
		if (this.beanFactory != null) {
			ajexp.setBeanFactory(this.beanFactory);
		}
		return ajexp;
	}

——>	protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) {
		// 看到熟悉的AOP切入點註解:@Before、@Around、@After、@AfterReturning、@AfterThrowing、@Pointcut
		Class<?>[] classesToLookFor = new Class<?>[] {
				Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class};
		// 若方法被其中某個標註,則爲切入點
		for (Class<?> c : classesToLookFor) {
			AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) c);
			if (foundAnnotation != null) {
				return foundAnnotation;
			}
		}
		// 非切入點方法,則會被過濾掉
		return null;
	}

  最終,根據AOP的切入點註解過濾出來的切面方法,將被構建成,InstantiationModelAwarePointcutAdvisorImpl。
InstantiationModelAwarePointcutAdvisorImpl類圖
  查看它的類圖可以發現其實現了Order接口,我們知道Order接口是Spring排序用的,這在我們講解IOC時提到過,接下來我們就看一下,Spring是如何對這些切入點進行排序的?這種排序可以保證方法最終的執行順序嗎?

Advice初排序的調用鏈源碼解析

  回到我們尋找所有Advice代碼處findEligibleAdvisors,接着進行初排序分析。

	protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
		AnnotationAwareOrderComparator.sort(advisors);
		return advisors;
	}

	// AspectJAwareAdvisorAutoProxyCreator類中的實現
——>	protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
		List<PartiallyComparableAdvisorHolder> partiallyComparableAdvisors =
				new ArrayList<>(advisors.size());
		for (Advisor element : advisors) {
			partiallyComparableAdvisors.add(
					new PartiallyComparableAdvisorHolder(element, DEFAULT_PRECEDENCE_COMPARATOR));
		}
		// 根據Order值進行排序,可通過@Order進行設置,order的值越小其優先級越高
		List<PartiallyComparableAdvisorHolder> sorted =
				PartialOrder.sort(partiallyComparableAdvisors);
		if (sorted != null) {
			List<Advisor> result = new ArrayList<>(advisors.size());
			for (PartiallyComparableAdvisorHolder pcAdvisor : sorted) {
				result.add(pcAdvisor.getAdvisor());
			}
			return result;
		}
		else {
			return super.sortAdvisors(advisors);
		}
	}

  可以看到,最終Order排序與切入點方法的執行順序並不相同,那麼Spring 又是如何保證切入點方法與代理方法的執行順序的呢,也就是誰前誰後?

遞歸實現的調用鏈源碼分析

  通過以上代碼分析,我們瞭解到我們可以通過@Order註解標註切面的執行順序,order的值越小其優先級越高。

  Advisor如何層層傳遞調用的?這一切都依仗於責任鏈設計模式。

  createProxy()經過層層調用將根據Bean實例是否是接口實現類判斷採用JDK或CgLib動態代理,被代理的對象實例的方法在執行時,將會執行invoke()或intercept()方法。(如有疑問,請先了解AOP原理)

  現在,我們就從invoke()/intercept()方法開始分析,調用鏈究竟如何實現的。

  不論是採用JDK或是CgLib,你會發現在它們各自的代理方法中都包括如下這段代碼,而這就是AOP Advisor 調用鏈的入口。

			// 獲取當前代理的方法的所有Advice。
			// 如果你細細研究,會發現在getInterceptorsAndDynamicInterceptionAdvice中,有一段代碼,
			// 將所有的AOP切面Advice轉換成了MethodInterceptor接口。
			// MethodInterceptor則是Spring所有切入點方法的祖先。
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
			if (chain.isEmpty()) {
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
			else {
				// 通過反射技術創建調用鏈執行方法...
				invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// 現在執行所有的攔截器和切面Advice.
				retVal = invocation.proceed();
			}

——>	public Object proceed() throws Throwable {
		// 執行完所有Advice後執行代理方法,currentInterceptorIndex 是標識調用鏈位置的偏移量。
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}
		// 獲取下一個要執行的Advice
		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
			// 執行動態方法匹配器
			InterceptorAndDynamicMethodMatcher dm =
					(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
			if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
			// InterceptorAndDynamicMethodMatcher 爲動態攔截器,根據運行時參數來決定攔截器是否生效,有動態攔截器當然也有靜態攔截器,靜態攔截器一般通過包名,類名,方法名 ,參數來確定靜態攔截器是否對代理方法生效
				return dm.interceptor.invoke(this);
			}
			else {
				// 動態方法匹配失敗,跳過此攔截器並調用鏈中的下一個Advice攔截器,遞歸執行。
				return proceed();
			}
		}
		else {
			// 執行靜態攔截器,我們的AOP 切入點都是此類攔截器(包括@DeclareParents)。
			// 這裏將this傳入過去,就是爲了實現遞歸責任鏈
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}
	}

  Ctrl+Alt+B 可以看到invoke(this)有非常多的實現,我們重點關注@Before、@Around、@After、@AfterReturning、@AfterThrowing、@Pointcut的實現。

  首先,我們需要了解它們各自的執行時機:

   @Before:前置通知,在方法執行之前運行;
   @After:後置通知,在方法返回結果之後運行;
   @AfterReturning:返回結果通知,方法返回結果之後運行,也攔截返回的結果;
   @Around:環繞通知,運行方法執行;
   @AfterThrowing:異常通知,在方法引發異常之後運行。

	// MethodBeforeAdviceInterceptor 前置通知
	public Object invoke(MethodInvocation mi) throws Throwable {
		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis() );
		return mi.proceed();
	}

	// AspectJAfterAdvice 後置通知
	public Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		finally {
			invokeAdviceMethod(getJoinPointMatch(), null, null);
		}
	}

	// AfterReturningAdviceInterceptor 返回結果通知
	public Object invoke(MethodInvocation mi) throws Throwable {
		Object retVal = mi.proceed();
		this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
		return retVal;
	}

	// AspectJAroundAdvice 環繞通知
	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);
		// 具體實現方式參閱invokeAdviceMethod部分代碼。
		return invokeAdviceMethod(pjp, jpm, null, null);
	}

	// AspectJAfterThrowingAdvice 異常通知
	public Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		catch (Throwable ex) {
			if (shouldInvokeOnThrowing(ex)) {
				invokeAdviceMethod(getJoinPointMatch(), null, ex);
			}
			throw ex;
		}
	}

   最終,Spring通過層層傳遞的this進行遞歸調用proceed(),通過currentInterceptorIndex偏移量記錄調用鏈執行位置,通過反射調用目標方法,進而實現層層傳遞,層層調用的責任鏈模型。

AOP 調用鏈總結

   1、第一步,Spring IOC Bean後置處理器執行階段,解析被@AspectJ註解標註的實例;

   2、第二步,解析@AspectJ標註的實例,這個過程是解析器內部的切入點、以及切面方法,最終將解析成MethodInterceptor接口1(當然這個過程還包括Spring其他攔截器的解析)。主要解析@Before、@Around、@After、@AfterReturning、@AfterThrowing、@Pointcut註解;

   3、第三步,根據order值,對MethodInterceptor排序,order的值越小其優先級越高。MethodInterceptor的order值等於它們的聲明(加載)順序,當然我們也可以通過@Order註解標註他們的順序,Order的大小決定它們最終的執行順序;

   4、第四步,當被代理類方法執行前,調用攔截鏈邏輯,主要依據下圖所示實現。

   4.1、使用一個列表(interceptorsAndDynamicMethodMatchers)存放排序後的MethodInterceptor;
   4.2、使用一個偏移量(currentInterceptorIndex)標識當前調用鏈執行下標;
   4.3、遞歸過程中傳遞this關鍵字保證以上兩個變量的全局一致;
   4.4、切入點邏輯依照不同類型的MethodInterceptor而選擇。例如,前置通知,在方法之前執行等;
   4.5、層層傳遞,責任調用。前一個切面代碼執行完交由下一個切面執行,形成鏈狀模型。

Spring AOP 遞歸調用鏈模型

事務爲何會失效?

public class TestTransactional {
    @Transactional(propagation = Propagation.REQUIRED)
    public void A() {
        User user = new User("chunsoft");
        userMapper.insertSelective(user);
        if (true) {
            throw new RuntimeException("拋異常");
        }
    }
    
    public void B() {
        this.A();
    }
}

   參考實例給出的代碼,這裏的事務爲何會失效呢?我們先查看一下事務的執行代碼,它也是MethodInterceptor的一種實現,上面我們介紹的AOP切面執行邏輯是一樣的,它的名字叫做TransactionInterceptor。

	public Object invoke(MethodInvocation invocation) throws Throwable {
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

		// 執行事務邏輯...
		return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
	}

	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

		// 如果transaction屬性爲null,則該方法爲非事務處理。.
		TransactionAttributeSource tas = getTransactionAttributeSource();
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
		// 創建事務管理器
		final PlatformTransactionManager tm = determineTransactionManager(txAttr);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
			// 開啓事務
			TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
			Object retVal = null;
			try {
				// 調用鏈中的下一個攔截器.
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// 事務回滾
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				// 重置TransactionInfo ThreadLocal
				cleanupTransactionInfo(txInfo);
			}
			// 提交
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}
		else {
			// 省略CallbackPreferringPlatformTransactionManager部分代碼……
		}
	}

   可以看到,最終事務通過反射進行調用,但若@Transactional標註的方法在同類內被調用,則事務不會生效,因爲Jdk/CgLib代理是基於對象的,需要在解析切入點是解析@Transactional註解的,同類調用,則代理不生效,事務不生效。


  1. 這裏之所以說解析成MethodInterceptor而不是Advice,是因爲MethodInterceptor繼承了Advice,便於我們理解記憶。 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章