Spring使用@Async出現(xiàn)循環(huán)依賴原因及解決方案分析
場(chǎng)景復(fù)現(xiàn)
1、首先項(xiàng)目需要打開spring的異步開關(guān),在application主類上加@EnableAsync
2、創(chuàng)建一個(gè)包含了@Async方法的異步類MessageService:
@Service public class MessageService { @Resource private TaskService taskService; @Async public void send(){ taskService.shit(); } }
3、創(chuàng)建另一個(gè)正常類TaskService,與異步類形成循環(huán)引用的關(guān)系(注意MessageService和TaskService在同一個(gè)包內(nèi),并且order為默認(rèn),因此會(huì)先掃描MessageService再掃描TaskService):
@Service public class TaskService { @Resource private MessageService messageService; public void shit(){ System.out.println(); } }
4、啟動(dòng)springboot項(xiàng)目成功報(bào)錯(cuò)
問題出現(xiàn)的原因
在分析原因之前,我們需要提前知道兩個(gè)重要的點(diǎn):
- spring的aop代理(包括@Transactional 事務(wù)代理),都是在AbstractAutowireCapableBeanFactory的populateBean方法后的initializeBean當(dāng)中的applyBeanPostProcessorsAfterInitialization方法里,通過特定的后置處理器里創(chuàng)建代理對(duì)象(如果用@Autowired則是AnnotationAwareAspectJAutoProxyCreator)
- 然而1.當(dāng)中描述的代理過程,是在這個(gè)類不涉及到循環(huán)引用的情況下才會(huì)執(zhí)行,也就是說滿足百分之90的情況,而循環(huán)引用的情況會(huì)做特殊的處理,即提前創(chuàng)建代理對(duì)象。
舉個(gè)例子: T類是個(gè)包含了@Transactional方法的類,屬于需要被代理的對(duì)象,并且通過@Resource(或者@Autowired)的方式依賴了A ,A類中也以同樣的方式注入了T,并且T類先于A類開始實(shí)例化過程,那么簡(jiǎn)單的實(shí)例化流程就是:
- T的BeanDefinition被spring拿到后,根據(jù)構(gòu)造器實(shí)例化一個(gè)T對(duì)象(原始對(duì)象而非代理對(duì)象),并包裝成objectFactory放入singletonFactories(三級(jí)緩存)中 然后執(zhí)行populateBean方法開始注入屬性的流程,其中會(huì)利用CommonAnnotationBeanPostProcessor(@Resource用這個(gè)后置處理器,@Autowired用 AutowiredAnnotationBeanPostProcessor)執(zhí)行T的屬性注入步驟,遍歷T中所依賴的屬性
- 發(fā)現(xiàn)T依賴了A,會(huì)先到beanFactory的一至三級(jí)緩存中,通過A的beanName查詢A對(duì)象,如果沒找到,即A還沒有被實(shí)例化過,那么會(huì)將A作為實(shí)例化的目標(biāo),重復(fù)a.步驟:將A實(shí)例化后的對(duì)象包裝成objectFactory放入singletonFactories,接著對(duì)A執(zhí)行populateBean來注入屬性
- 遍歷A的屬性,發(fā)現(xiàn)A依賴了T,然后嘗試去beanFactory中獲取T的實(shí)例,發(fā)現(xiàn)三級(jí)緩存中存在T的objectFactory,因此執(zhí)行objectFactory.getObject方法企圖獲取T的實(shí)例。然而這個(gè)objectFactory并非是簡(jiǎn)單把對(duì)象返回出去,而是在當(dāng)初包裝的時(shí)候,就將AbstractAutowireCapableBeanFactory的getEarlyBeanReference方法寫入getObject當(dāng)中
- 在getEarlyBeanReference方法里,會(huì)遍歷所有SmartInstantiationAwareBeanPostProcessor的子類型的后置處理器,執(zhí)行對(duì)應(yīng)的getEarlyBeanReference方法,此時(shí)會(huì)將第1.點(diǎn)提到的代理過程提前,即通過 AnnotationAwareAspectJAutoProxyCreator(SmartInstantiationAwareBeanPostProcessor的子類)來創(chuàng)建一個(gè)代理對(duì)象,并放入二級(jí)緩存earlySingletonObjects當(dāng)中,然后將這個(gè)代理對(duì)象通過field.set的形式(默認(rèn)形式)注入到A,至此就完成了普通aop對(duì)象的循環(huán)引用處理
出現(xiàn)本文標(biāo)題中循環(huán)引用異常的原因分析
包含了@Async 方法的類與@Transactional的類相似,也會(huì)被替換成一個(gè)新的代理類,但是與普通aop不同的是,@Async不會(huì)在 getEarlyBeanReference 階段執(zhí)行創(chuàng)建代理的邏輯(這么做的原因暫時(shí)沒仔細(xì)分析),而是被延遲到了initializeBean步驟當(dāng)中(即1.提到的90%的代理情況),這樣一來就會(huì)導(dǎo)致TaskService注入的并不是最終創(chuàng)建完成的MessageService的代理對(duì)象,很明顯這樣的結(jié)果是不合理的,而在代碼層面,spring的AbstractAutowireCapableBeanFactory當(dāng)中,在initializeBean和將bean放入一級(jí)緩存之間,有這么一段容易被忽視的代碼,用于把控最終的循環(huán)引用結(jié)果正確性:
//是否允許提前暴露,可以理解為是否允許循環(huán)引用 if (earlySingletonExposure) { //遍歷一到三級(jí)緩存,拿到的bean Object earlySingletonReference = getSingleton(beanName, false); //如果緩存中的對(duì)象不為空 if (earlySingletonReference != null) { //exposedObject是執(zhí)行了initializeBean之后的對(duì)象,bean是通過構(gòu)造器創(chuàng)建的原始對(duì)象 //如果兩者相等,則將exposedObject設(shè)置為緩存中的對(duì)象 if (exposedObject == bean) { exposedObject = earlySingletonReference; } //如果兩者不是同一個(gè)對(duì)象,并且不允許直接注入原生對(duì)象(默認(rèn)false),且當(dāng)前beanName有被其他的bean所依賴 else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { //則獲取所有依賴了該beanName的對(duì)象 String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); for (String dependentBean : dependentBeans) { //如果這個(gè)對(duì)象已經(jīng)處于一級(jí)緩存當(dāng)中,則添加到actualDependentBeans,即依賴該對(duì)象的bean是一個(gè)走完了整個(gè)流程,不會(huì)再有機(jī)會(huì)回爐重做的bean if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } //最后判斷actualDependentBeans是否為空,不為空就拋循環(huán)引用的異常 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."); } } } }
- 我們結(jié)合這段代碼來分析@Async 循環(huán)引用場(chǎng)景:
- 先看第4行,首先這個(gè)時(shí)候肯定還沒進(jìn)入一級(jí)緩存,而我們知道@Async在 getEarlyBeanReference 中并沒有執(zhí)行代理,因此第4行獲取到的 earlySingletonReference 是messageService的原始對(duì)象
- 進(jìn)入第9行,判斷exposedObject == bean,由于@Async的代理過程發(fā)生在initializeBean中, 因此exposedObject是代理對(duì)象,而bean是通過構(gòu)造器直接實(shí)例化的原始對(duì)象,因此肯定不相等
- 進(jìn)入第12行,allowRawInjectionDespiteWrapping默認(rèn)為false,而messageService是被TaskService所引用的,因此 hasDependentBean (beanName)為true ,會(huì)進(jìn)入14行代碼塊
- 重點(diǎn)是判斷18行的 ! removeSingletonIfCreatedForTypeCheckOnly (dependentBean),該方法代碼為:
protected boolean removeSingletonIfCreatedForTypeCheckOnly(String beanName) { //如果不是已經(jīng)完全創(chuàng)建好的bean,就返回true,否則返回false if (!this.alreadyCreated.contains(beanName)) { removeSingleton(beanName); return true; } else { return false; } }
這里就要回到場(chǎng)景復(fù)現(xiàn)時(shí)提到的:
3、注意MessageService和TaskService在同一個(gè)包內(nèi),并且order為默認(rèn),因此會(huì)先掃描MessageService再掃描TaskService。
- 由于messageService先被掃描,因此會(huì)在messageService的populateBean當(dāng)中,執(zhí)行TaskService的實(shí)例化過程,而TaskService此時(shí)并不知道m(xù)essageService是一個(gè)需要代理的類,因此將一個(gè)未代理的messageService注入之后,心安理得地執(zhí)行了initializeBean以及后續(xù)的初始化操作,然后標(biāo)記為成功創(chuàng)建并裝入一級(jí)緩存。
- 也就是說,此時(shí)spring判斷TaskService是一個(gè)已經(jīng)完全實(shí)例化并初始化完成的對(duì)象。因此removeSingletonIfCreatedForTypeCheckOnly方法會(huì)返回false,則18行返回的是true,所以TaskService會(huì)被加入到actualDependentBeans當(dāng)中,最終拋出BeanCurrentlyInCreationException異常
- 簡(jiǎn)單來說,spring認(rèn)為如果一個(gè)bean在initializeBean前后不一致,并且一個(gè)已經(jīng)完全初始化的beanA注入了這個(gè)未完全初始化的beanB,在spring的流程中beanA就再也沒有機(jī)會(huì)改變注入的依賴了,所以會(huì)拋異常。
- 而如果先實(shí)例化TaskService再實(shí)例化MessageService,就不會(huì)有這個(gè)問題(不信可以將TaskService改成ATaskService試試),因?yàn)槿绻趯?shí)例化TaskService的時(shí)候沒有發(fā)現(xiàn)提前暴露出來的MessageService,就會(huì)專注于創(chuàng)建MessageService的過程,實(shí)例化并初始化完成后才會(huì)回到TaskService并將MessageService注入
為什么@Lazy可以解決這個(gè)問題
@Lazy 被大多數(shù)人理解為:當(dāng)使用到的時(shí)候才會(huì)加載這個(gè)類。
這個(gè)也算是spring希望我們看到的,但是這個(gè)描述實(shí)際上不完全準(zhǔn)確。舉個(gè)例子:
@Service public class TaskService { @Resource @Lazy private MessageService messageService; public void shit(){ System.out.println(); } }
- 這里在messageService屬性上面加了@Lazy。在實(shí)例化TaskService,并populateBean的時(shí)候,在 CommonAnnotationBeanPostProcessor 的 getResourceToInject方法中, spring發(fā)現(xiàn)messageService被@Lazy注解修飾,便會(huì)將其包裝成一個(gè)代理對(duì)象:即創(chuàng)建一個(gè)TargetSource,重寫getTarget方法,返回的是 CommonAnnotationBeanPostProcessor 里的 getResource(beanName)方法(方法體中的邏輯,可以理解為從工廠的三層緩存中獲取對(duì)象)。也就是說,注入給TaskService的是一個(gè)MessageService的代理對(duì)象(這是本文出現(xiàn)的第三種代理場(chǎng)景)。
- 而spring在實(shí)例化MessageService的時(shí)候,不會(huì)管他是否是由@Lazy 修飾的,只會(huì)將其當(dāng)做一個(gè)普通的bean去創(chuàng)建,成功后就會(huì)放入一級(jí)緩存(所以嚴(yán)格來講,不能說是“使用到了再去加載”)。
- 容器啟動(dòng)完成后,TaskService在需要使用messageService的方法時(shí),會(huì)執(zhí)行代理對(duì)象的邏輯,獲取到TargetSource,調(diào)用getResource從三層緩存中獲取messageService的真實(shí)對(duì)象,由于messageService此時(shí)已經(jīng)被spring完整地創(chuàng)建好了,處于一級(jí)緩存singletonObjects當(dāng)中,因此拿到之后可以放心使用。
到此這篇關(guān)于Spring使用@Async出現(xiàn)循環(huán)依賴原因以及解決方案的文章就介紹到這了,更多相關(guān)Spring @Async循環(huán)依賴內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot Nacos實(shí)現(xiàn)自動(dòng)刷新
這篇文章主要介紹了SpringBoot Nacos實(shí)現(xiàn)自動(dòng)刷新,Nacos(Dynamic Naming and Configuration Service)是阿里巴巴開源的一個(gè)動(dòng)態(tài)服務(wù)發(fā)現(xiàn)、配置管理和服務(wù)管理平臺(tái)2023-01-01Java如何在臨界區(qū)中避免競(jìng)態(tài)條件
這篇文章主要介紹了Java如何在臨界區(qū)中避免競(jìng)態(tài)條件,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10Java兩個(gè)乒乓球隊(duì)比賽名單問題(判斷素?cái)?shù))
兩個(gè)乒乓球隊(duì)進(jìn)行比賽,各出三人。甲隊(duì)為a,b,c三人,乙隊(duì)為x,y,z三人。已抽簽決定比賽名單。有人向隊(duì)員打聽比賽的名單。a說他不和x比,c說他不和x,z比,請(qǐng)編程序找出三隊(duì)賽手的名單2017-02-02關(guān)于Lombok @Data注解:簡(jiǎn)化Java代碼的魔法棒
Lombok庫通過@Data注解自動(dòng)生成常見的樣板代碼如getter、setter、toString等,極大減少代碼量,提高開發(fā)效率,@Data注解集成了@ToString、@EqualsAndHashCode、@Getter、@Setter、@RequiredArgsConstructor等注解的功能2024-10-10解決maven中只有Lifecycle而Dependencies和Plugins消失的問題
這篇文章主要介紹了maven中只有Lifecycle而Dependencies和Plugins消失的問題及解決方法,本文通過圖文的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-07-07SpringBoot項(xiàng)目的兩種發(fā)布方式
本文主要介紹了SpringBoot項(xiàng)目的兩種發(fā)布方式,包含jar包發(fā)布和war包發(fā)布,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-07-07