@Async註解的坑,小心

三友的java日記發表於2022-07-12

大家好,我是三友。

背景

前段時間,一個同事小姐姐跟我說她的專案起不來了,讓我幫忙看一下,本著助人為樂的精神,這個忙肯定要去幫。

於是,我在她的控制檯發現瞭如下的異常資訊:

Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495)
 at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317)
 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)

看到BeanCurrentlyInCreationException這個異常,我的第一反應是出現了迴圈依賴的問題。但是仔細一想,Spring不是已經解決了迴圈依賴的問題麼,怎麼還報這個錯。於是,我就詢問小姐姐改了什麼東西,她說在方法上加了@Async註解。

這裡我模擬一下當時的程式碼,AService 和 BService 相互引用,AService的 save() 方法加了 @Async 註解。

@Component
public class AService {
    @Resource
    private BService bService;

    @Async
    public void save() {

    }
}

@Component
public class BService {

    @Resource
    private AService aService;

}

也就是這段程式碼會報BeanCurrentlyInCreationException異常,難道是@Async註解遇上迴圈依賴的時候,Spring無法解決?為了驗證這個猜想,我將@Async註解去掉之後,再次啟動專案,專案成功起來了。於是基本可以得出結論,那就是@Async註解遇上迴圈依賴的時候,Spring的確無法解決。

雖然問題的原因已經找到了,但是又引出以下幾個問題:

  • @Async註解是如何起作用的?
  • 為什麼@Async註解遇上迴圈依賴,Spring無法解決?
  • 出現迴圈依賴異常之後如何解決?

@Async註解是如何起作用的?

@Async註解起作用是靠AsyncAnnotationBeanPostProcessor這個類實現的,這個類會處理@Async註解。AsyncAnnotationBeanPostProcessor這個類的物件是由@EnableAsync註解放入到Spring容器的,這也是為什麼需要使用@EnableAsync註解來啟用讓@Async註解起作用的根本原因。

AsyncAnnotationBeanPostProcessor

@Async註解的坑,小心類體系

這個類實現了 BeanPostProcessor 介面,實現了 postProcessAfterInitialization 方法,是在其父類AbstractAdvisingBeanPostProcessor 中實現的,也就是說當Bean的初始化階段完成之後會回撥 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法。之所以會回撥,是因為在Bean的生命週期中,當Bean初始化完成之後,會回撥所有的 BeanPostProcessor 的 postProcessAfterInitialization 方法,程式碼如下:

@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)throws BeansException {
    Object result = existingBean;
    for (BeanPostProcessor processor : getBeanPostProcessors()) {
         Object current = processor.postProcessAfterInitialization(result, beanName);
         if (current == null) {
            return result;
         }
         result = current;
     }
    return result;
}

AsyncAnnotationBeanPostProcessor 對於 postProcessAfterInitialization 方法實現:

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (this.advisor == null || bean instanceof AopInfrastructureBean) {
   // Ignore AOP infrastructure such as scoped proxies.
        return bean;
    }

    if (bean instanceof Advised) {
       Advised advised = (Advised) bean;
       if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
           // Add our local Advisor to the existing proxy's Advisor chain...
           if (this.beforeExistingAdvisors) {
              advised.addAdvisor(0, this.advisor);
           }
           else {
              advised.addAdvisor(this.advisor);
           }
           return bean;
        }
     }

     if (isEligible(bean, beanName)) {
        ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
        if (!proxyFactory.isProxyTargetClass()) {
           evaluateProxyInterfaces(bean.getClass(), proxyFactory);
        }
        proxyFactory.addAdvisor(this.advisor);
        customizeProxyFactory(proxyFactory);
        return proxyFactory.getProxy(getProxyClassLoader());
     }

     // No proxy needed.
     return bean;
}

該方法的主要作用是用來對方法入參的物件進行動態代理的,當入參的物件的類加了@Async註解,那麼這個方法就會對這個物件進行動態代理,最後會返回入參物件的代理物件出去。至於如何判斷方法有沒有加@Async註解,是靠 isEligible(bean, beanName) 來判斷的。由於這段程式碼牽扯到動態代理底層的知識,這裡就不詳細展開了。

@Async註解的坑,小心AsyncAnnotationBeanPostProcessor作用

綜上所述,可以得出一個結論,那就是當Bean建立過程中初始化階段完成之後,會呼叫 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 的方法,對加了@Async註解的類的物件進行動態代理,然後返回一個代理物件回去。

雖然這裡我們得出@Async註解的作用是依靠動態代理實現的,但是這裡其實又引發了另一個問題,那就是事務註解@Transactional又或者是自定義的AOP切面,他們也都是通過動態代理實現的,為什麼使用這些的時候,沒見丟擲迴圈依賴的異常?難道他們的實現跟@Async註解的實現不一樣?不錯,還真的不太一樣,請繼續往下看。

AOP是如何實現的?

我們都知道AOP是依靠動態代理實現的,而且是在Bean的生命週期中起作用,具體是靠 AnnotationAwareAspectJAutoProxyCreator 這個類實現的,這個類會在Bean的生命週期中去處理切面,事務註解,然後生成動態代理。這個類的物件在容器啟動的時候,就會被自動注入到Spring容器中。

AnnotationAwareAspectJAutoProxyCreator 也實現了BeanPostProcessor,也實現了 postProcessAfterInitialization 方法。

@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;
}

通過 wrapIfNecessary 方法就會對Bean進行動態代理,如果你的Bean需要被動態代理的話。

@Async註解的坑,小心AnnotationAwareAspectJAutoProxyCreator作用

也就說,AOP和@Async註解雖然底層都是動態代理,但是具體實現的類是不一樣的。一般的AOP或者事務的動態代理是依靠 AnnotationAwareAspectJAutoProxyCreator 實現的,而@Async是依靠 AsyncAnnotationBeanPostProcessor 實現的,並且都是在初始化完成之後起作用,這也就是@Async註解和AOP之間的主要區別,也就是處理的類不一樣。

Spring是如何解決迴圈依賴的

Spring在解決迴圈依賴的時候,是依靠三級快取來實現的。我曾經寫過一篇關於三級快取的文章,如果有不清楚的小夥伴可以 關注微信公眾號 三友的java日記,回覆 迴圈依賴 即可獲取原文連結,本文也算是這篇三級快取文章的續作。

簡單來說,通過快取正在建立的物件對應的ObjectFactory物件,可以獲取到正在建立的物件的早期引用的物件,當出現迴圈依賴的時候,由於物件沒建立完,就可以通過獲取早期引用的物件注入就行了。

而快取ObjectFactory程式碼如下:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
       if (!this.singletonObjects.containsKey(beanName)) {
           this.singletonFactories.put(beanName, singletonFactory);
           this.earlySingletonObjects.remove(beanName);
           this.registeredSingletons.add(beanName);
       }
    }
}

所以快取的ObjectFactory物件其實是一個lamda表示式,真正獲取早期暴露的引用物件其實就是通過 getEarlyBeanReference 方法來實現的。

getEarlyBeanReference 方法:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
               SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
               exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
       }
    }
    return exposedObject;
}

getEarlyBeanReference 實現是呼叫所有的 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 方法。

而前面提到的 AnnotationAwareAspectJAutoProxyCreator 這個類就實現了 SmartInstantiationAwareBeanPostProcessor 介面,是在父類中實現的:

@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    if (!this.earlyProxyReferences.contains(cacheKey)) {
        this.earlyProxyReferences.add(cacheKey);
    }
    return wrapIfNecessary(bean, beanName, cacheKey);
}

這個方法最後會呼叫 wrapIfNecessary 方法,前面也說過,這個方法是獲取動態代理的方法,如果需要的話就會代理,比如事務註解又或者是自定義的AOP切面,在早期暴露的時候,就會完成動態代理。

這下終於弄清楚了,早期暴露出去的原來可能是個代理物件,而且最終是通過AnnotationAwareAspectJAutoProxyCreator這個類的getEarlyBeanReference方法獲取的。

但是AsyncAnnotationBeanPostProcessor並沒有實現SmartInstantiationAwareBeanPostProcessor,也就是在獲取早期物件這一階段,並不會調AsyncAnnotationBeanPostProcessor處理@Async註解。

為什麼@Async註解遇上迴圈依賴,Spring無法解決?

這裡我們就拿前面的例子來說,AService加了@Async註解,AService先建立,發現引用了BService,那麼BService就會去建立,當Service建立的過程中發現引用了AService,那麼就會通過AnnotationAwareAspectJAutoProxyCreator 這個類實現的 getEarlyBeanReference 方法獲取AService的早期引用物件,此時這個早期引用物件可能會被代理,取決於AService是否需要被代理,但是一定不是處理@Async註解的代理,原因前面也說過。

於是BService建立好之後,注入給了AService,那麼AService會繼續往下處理,前面說過,當初始化階段完成之後,會呼叫所有的BeanPostProcessor的實現的 postProcessAfterInitialization 方法。於是就會回撥依次回撥 AnnotationAwareAspectJAutoProxyCreator 和 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法實現。

這段回撥有兩個細節:

  • AnnotationAwareAspectJAutoProxyCreator 先執行,AsyncAnnotationBeanPostProcessor 後執行,因為 AnnotationAwareAspectJAutoProxyCreator 在前面。
@Async註解的坑,小心
  • AnnotationAwareAspectJAutoProxyCreator處理的結果會當入參傳遞給 AsyncAnnotationBeanPostProcessor,applyBeanPostProcessorsAfterInitialization方法就是這麼實現的

AnnotationAwareAspectJAutoProxyCreator回撥:會發現AService物件已經被早期引用了,什麼都不處理,直接把物件AService給返回

AsyncAnnotationBeanPostProcessor回撥:發現AService類中加了@Async註解,那麼就會對AnnotationAwareAspectJAutoProxyCreator返回的物件進行動態代理,然後返回了動態代理物件。

這段回撥完,是不是已經發現了問題。早期暴露出去的物件,可能是AService本身或者是AService的代理物件,而且是通過AnnotationAwareAspectJAutoProxyCreator物件實現的,但是通過AsyncAnnotationBeanPostProcessor的回撥,會對AService物件進行動態代理,這就導致AService早期暴露出去的物件跟最後完全創造出來的物件不是同一個,那麼肯定就不對了。同一個Bean在一個Spring中怎麼能存在兩個不同的物件呢,於是就會丟擲BeanCurrentlyInCreationException異常,這段判斷邏輯的程式碼如下:

if (earlySingletonExposure) {
  // 獲取到早期暴露出去的物件
  Object earlySingletonReference = getSingleton(beanName, false);
  if (earlySingletonReference != null) {
      // 早期暴露的物件不為null,說明出現了迴圈依賴
      if (exposedObject == bean) {
          // 這個判斷的意思就是指 postProcessAfterInitialization 回撥沒有進行動態代理,如果沒有那麼就將早期暴露出去的物件賦值給最終暴露(生成)出去的物件,
          // 這樣就實現了早期暴露出去的物件和最終生成的物件是同一個了
          // 但是一旦 postProcessAfterInitialization 回撥生成了動態代理 ,那麼就不會走這,也就是加了@Aysnc註解,是不會走這的
          exposedObject = earlySingletonReference;
      }
      else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
               // allowRawInjectionDespiteWrapping 預設是false
               String[] dependentBeans = getDependentBeans(beanName);
               Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
               for (String dependentBean : dependentBeans) {
                   if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                       actualDependentBeans.add(dependentBean);
                  }
               }
               if (!actualDependentBeans.isEmpty()) {
                   //丟擲異常
                   throw new BeanCurrentlyInCreationException(beanName,
                           "Bean with name '" + beanName + "' has been injected into other beans [" +
                           StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                           "] in its raw version as part of a circular reference, but has eventually been " +
                           "wrapped. This means that said other beans do not use the final version of the " +
                           "bean. This is often the result of over-eager type matching - consider using " +
                           "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
               }
      }
   }
}

所以,之所以@Async註解遇上迴圈依賴,Spring無法解決,是因為@Aysnc註解會使得最終建立出來的Bean,跟早期暴露出去的Bean不是同一個物件,所以就會報錯。

出現迴圈依賴異常之後如何解決?

解決這個問題的方法很多

1、調整物件間的依賴關係,從根本上杜絕迴圈依賴,沒有迴圈依賴,就沒有早期暴露這麼一說,那麼就不會出現問題

2、不使用@Async註解,可以自己通過執行緒池實現非同步,這樣沒有@Async註解,就不會在最後生成代理物件,導致早期暴露的出去的物件不一樣

3、可以在迴圈依賴注入的欄位上加@Lazy註解

@Component
public class AService {
    @Resource
    @Lazy
    private BService bService;

    @Async
    public void save() {

    }
}

4、從上面的那段判斷拋異常的原始碼註釋可以看出,當allowRawInjectionDespiteWrapping為true的時候,就不會走那個else if,也就不會丟擲異常,所以可以通過將allowRawInjectionDespiteWrapping設定成true來解決報錯的問題,程式碼如下:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        ((DefaultListableBeanFactory) beanFactory).setAllowRawInjectionDespiteWrapping(true);
    }
    
}

雖然這樣設定能解決報錯的問題,但是並不推薦,因為這樣設定就允許早期注入的物件和最終建立出來的物件是不一樣,並且可能會導致最終生成的物件沒有被動態代理。

如果覺得這篇文章對你有所幫助,還請幫忙點贊、在看、轉發給更多的人,非常感謝!

往期熱門文章推薦

掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。

 

相關文章