spring boot項目使用@Async註解的坑
背景
前段時間,一個同事小姐姐跟我說她的項目起不來瞭,讓我幫忙看一下,本著助人為樂的精神,這個忙肯定要去幫。
於是,我在她的控制臺發現瞭如下的異常信息:
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
類體系
這個類實現瞭 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) 來判斷的。由於這段代碼牽扯到動態代理底層的知識,這裡就不詳細展開瞭。
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需要被動態代理的話。
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 在前面。
- 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); } }
雖然這樣設置能解決報錯的問題,但是並不推薦,因為這樣設置就允許早期註入的對象和最終創建出來的對象是不一樣,並且可能會導致最終生成的對象沒有被動態代理。
以上就是spring boot項目使用@Async註解的坑的詳細內容,更多關於spring boot項目@Async註解的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Spring處理@Async導致的循環依賴失敗問題的方案詳解
- 深度解析SpringBoot中@Async引起的循環依賴
- 聊聊Spring循環依賴三級緩存是否可以減少為二級緩存的情況
- Java之SSM中bean相關知識匯總案例講解
- 關於spring循環依賴問題及解決方案