深度解析SpringBoot中@Async引起的循環(huán)依賴
啊,昨晚發(fā)版又出現(xiàn)了讓有頭大的循環(huán)依賴問題,按理說Spring會為我們解決循環(huán)依賴,但是為什么還會出現(xiàn)這個問題呢?為什么在本地、UAT以及PRE環(huán)境都沒有出現(xiàn)這個問題,但是到了PROD環(huán)境就出現(xiàn)了這個問題呢?本文將從事故時間線、及時止損、復盤分析等幾個方面為大家?guī)碓敿毜姆治?,干貨滿滿!
事故時間線
本著"先止損、后復盤分析"
的原則,我們來看一下這次發(fā)版事故的時間線。
2021年11月16日晚23點00分00秒開始發(fā)版,此時集團的devops有點慢
2021年11月16日晚23點03分01秒,收到發(fā)版失敗的消息,登錄服務器發(fā)現(xiàn)發(fā)生了循環(huán)依賴,具體錯誤如下圖,從日志中可以看到是dataCollectionSendMessageService
這個bean出現(xiàn)了循環(huán)依賴
問題發(fā)現(xiàn)了就需要先解決,然后再去分析為什么。看到這個報錯日志我心里也大概知道是為什么了,所以很快就解決了,解決方案如下:給DataCollectionSendMessageService
加上@Lazy
注解
2021年11月16日晚23點07分16秒,使用重新集成的代碼開始發(fā)版,大概10分鐘后線上節(jié)點全部發(fā)版完成。從時間線來看從發(fā)現(xiàn)問題到解決問題,前后一共用了接近15分鐘(這期間代碼集成和發(fā)布用了過多的時間),也算是做到了及時止損,沒有讓問題繼續(xù)擴大。
猜想
我大膽的猜想是因為打了@Aysnc注解
的bean生成了對象的代理,導致Spring bean最終加載的不是一個原始對象導致了此次問題的發(fā)生,那么對不對呢,接下來我們通過源碼詳細分析一下。
什么是循環(huán)依賴
所謂循環(huán)依賴就是Spring IOC容器
在加載bean時會按照順序加載,先去實例化 beanA。然后發(fā)現(xiàn) beanA 依賴于 beanB,接在又去實例化 beanB。實例化 beanB 時,發(fā)現(xiàn) beanB 又依賴于 beanA。如果容器不處理循環(huán)依賴的話,容器會無限執(zhí)行上面的流程,直到內(nèi)存溢出,程序崩潰,所以這個時候就會拋出BeanCurrentlyInCreationException
異常,也就是我們常說的循環(huán)依賴,下面是兩種常見循環(huán)依賴的場景。
幾個Bean之間的循環(huán)依賴
@Component public class A { @Autowired private B b; } @Component public class B { @Autowired private C c; } @Component public class C { @Autowired private A a; }
效果圖如下:
自己依賴自己
@Component public class A { @Autowired private A a; }
效果圖如下:
Spring是如何解決循環(huán)依賴的
首先Spring維護了三個Map,也就是我們通常說的三級緩存
singletonObjects
:俗稱單例池,緩存創(chuàng)建完成的單例BeansingletonFactories
:映射創(chuàng)建Bean的原始工廠earlySingletonObjects
:映射Bean的早期引用,也就是說這個Map里的Bean不是完整的,只是完成了實例化,但還沒有初始化
Spring通過三級緩存解決了循環(huán)依賴,其中一級緩存為單例池(singletonObjects),二級緩存為早期曝光對象earlySingletonObjects,三級緩存為早期曝光對象工廠(singletonFactories)。
當A、B兩個類發(fā)生循環(huán)引用時,在A完成實例化后,就使用實例化后的對象去創(chuàng)建一個對象工廠,并添加到三級緩存中,如果A被AOP代理,那么通過這個工廠獲取到的就是A代理后的對象,如果A沒有被AOP代理,那么這個工廠獲取到的就是A實例化的對象
。
當A進行屬性注入時,會去創(chuàng)建B,同時B又依賴了A,所以創(chuàng)建B的同時又會去調(diào)用getBean(a)來獲取需要的依賴,此時的getBean(a)會從緩存中獲取,第一步,先獲取到三級緩存中的工廠;第二步,調(diào)用對象工工廠的getObject方法來獲取到對應的對象,得到這個對象后將其注入到B中。
緊接著B會走完它的生命周期流程,包括初始化、后置處理器等。當B創(chuàng)建完后,會將B再注入到A中,此時A再完成它的整個生命周期。至此,循環(huán)依賴結(jié)束!
簡單一句話說:先去緩存里找Bean,沒有則實例化當前的Bean放到Map,如果有需要依賴當前Bean的,就能從Map取到。
什么是@Async
@Async
注解是Spring為我們提供的異步調(diào)用的注解,@Async
可以作用到類或者方法上,標記了@Async
注解的方法將會在獨立的線程中被執(zhí)行,調(diào)用者無需等待它的完成,即可繼續(xù)其他的操作。從源碼中可以看到標記了@Async
注解的方法會被提交到org.springframework.core.task.TaskExecutor
中異步執(zhí)行。
或者我們可以通過value
來指定使用哪個自定義線程池,比如這樣子:
@Async("asyncTaskExecutor")
被@Async標記的bean注入時機
我們從源碼的角度來看一下被@Async
標記的bean是如何注入到Spring容器里的。在我們開啟@EnableAsync
注解之后代表可以向Spring容器中注入AsyncAnnotationBeanPostProcessor
,它是一個后置處理器,我們看一下他的類圖。
真正創(chuàng)建代理對象的代碼在AbstractAdvisingBeanPostProcessor
中的postProcessAfterInitialization
方法中,以下代碼有所刪減,只保留核心邏輯代碼
// 這個map用來緩存所有被postProcessAfterInitialization這個方法處理的bean private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256); // 這個方法主要是為打了@Async注解的bean生成代理對象 @Override public Object postProcessAfterInitialization(Object bean, String beanName) { // 這里是重點,這里返回true if (isEligible(bean, beanName)) { // 工廠模式生成一個proxyFactory ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); if (!proxyFactory.isProxyTargetClass()) { evaluateProxyInterfaces(bean.getClass(), proxyFactory); } // 切入切面并創(chuàng)建一個代理對象 proxyFactory.addAdvisor(this.advisor); customizeProxyFactory(proxyFactory); return proxyFactory.getProxy(getProxyClassLoader()); } // No proxy needed. return bean; }
protected boolean isEligible(Class<?> targetClass) { // 首次從eligibleBeans這個map中一定是拿不到的 Boolean eligible = this.eligibleBeans.get(targetClass); if (eligible != null) { return eligible; } // 如果沒有advisor,也就是切面,直接返回false if (this.advisor == null) { return false; } // 這里判斷AsyncAnnotationAdvisor能否切入,因為我們的bean是打了@Aysnc注解,這里是一定能切入的,最終會返回true eligible = AopUtils.canApply(this.advisor, targetClass); this.eligibleBeans.put(targetClass, eligible); return eligible; }
至此打了@Aysnc注解
的bean就創(chuàng)建完成了,結(jié)果是生成了一個代理對象
。
循環(huán)依賴到底是怎么生成的
經(jīng)過上面的源碼分析,我們可以知道有@Aysnc注解
的bean最后生成了一個代理對象,我們結(jié)合Spring bean創(chuàng)建的流程來分析這次問題。
beanA
開始初始化,beanA
實例化完成后給beanA
的依賴屬性beanB
進行賦值beanB
開始初始化,beanB
實例化完成后給beanB
的依賴屬性beanA
進行賦值- 因為
beanA
是支持循環(huán)依賴的,所以可以在earlySingletonObjects
中可以拿到beanA
的早期引用的,但是因為beanB
打了@Aysnc注解
并不能在earlySingletonObjects
中可以拿到早期引用 - 接下來執(zhí)行執(zhí)行
initializeBean(Object existingBean, String beanName)
方法,這里beanA
可以正常實例化完成,但是因為beanB
打了@Aysnc注解
,所以向Spring IOC容器中增加了一個代理對象,也就是說beanA
的beanB
并不是一個原始對象,而是一個代理對象 - 接下來進行執(zhí)行
doCreateBean
方法時對進行檢測,以下代碼有所刪減,只保留核心邏輯代碼
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { if (earlySingletonExposure) { Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { if (exposedObject == bean) { exposedObject = earlySingletonReference; } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); // 重點在這里,這里會遍歷所有依賴的bean,如果beanA依賴beanB和緩存中的beanB不相等 // 也就是說beanA本來依賴的是一個原始對象beanB,但是這個時候發(fā)現(xiàn)beanB是一個代理對象,就會增加到actualDependentBeans for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } // 發(fā)現(xiàn)actualDependentBeans不為空,就發(fā)生了我們最開始截圖的錯誤 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."); } } } } // Register bean as disposable. try { registerDisposableBeanIfNecessary(beanName, bean, mbd); } catch (BeanDefinitionValidationException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); } return exposedObject; }
解決循環(huán)依賴的正確姿勢
@Lazy
注解- 代碼優(yōu)化,不要讓
@Async
的Bean參與循環(huán)依賴
至此我們就知道為什么發(fā)生了此次問題。
到此這篇關(guān)于深度解析SpringBoot中@Async引起的循環(huán)依賴的文章就介紹到這了,更多相關(guān)SpringBoot中@Async循環(huán)依賴內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot + Jpa(Hibernate) 架構(gòu)基本配置詳解
本篇文章主要介紹了Spring Boot + Jpa(Hibernate) 架構(gòu)基本配置詳解,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05