Spring如何解決循環(huán)依賴的問題
前言
在面試的時候這兩年有一個非常高頻的關(guān)于spring的問題,那就是spring是如何解決循環(huán)依賴的。這個問題聽著就是輕描淡寫的一句話,其實考察的內(nèi)容還是非常多的,主要還是考察的應(yīng)聘者有沒有研究過spring的源碼。但是說實話,spring的源碼其實非常復(fù)雜的,研究起來并不是個簡單的事情,所以我們此篇文章只是為了解釋清楚Spring是如何解決循環(huán)依賴的這個問題。
什么樣的依賴算是循環(huán)依賴?
用過Spring框架的人都對依賴注入這個詞不陌生,一個Java類A中存在一個屬性是類B的一個對象,那么我們就說類A的對象依賴類B,而在Spring中是依靠的IOC來實現(xiàn)的對象注入,也就是說創(chuàng)建對象的過程是IOC容器來實現(xiàn)的,并不需要自己在使用的時候通過new關(guān)鍵字來創(chuàng)建對象。
那么當(dāng)類A中依賴類B的對象,而類B中又依賴類C的對象,最后類C中又依賴類A的對象的時候,這種情況最終的依賴關(guān)系會形成一個環(huán),這就是循環(huán)依賴。
循環(huán)依賴的類型
根據(jù)注入的時機(jī)可以分為兩種:
構(gòu)造器循環(huán)依賴
依賴的對象是通過構(gòu)造方法傳入的,在實例化bean的時候發(fā)生。
賦值屬性循環(huán)依賴
依賴的對象是通過setter方法傳入的,對象已經(jīng)實例化,在屬性賦值和依賴注入的時候發(fā)生。
構(gòu)造器循環(huán)依賴,本質(zhì)上是無解的,實例化A的時候調(diào)用A的構(gòu)造器,發(fā)現(xiàn)依賴了B,又去實例化B,然后調(diào)用B的構(gòu)造器,發(fā)現(xiàn)又依賴的C,然后調(diào)用C的構(gòu)造器去實例化,結(jié)果發(fā)起C的構(gòu)造器里依賴了A,這就是個死循環(huán)無解。所以Spring也是不支持構(gòu)造器循環(huán)依賴的,當(dāng)發(fā)現(xiàn)存在構(gòu)造器循環(huán)依賴時,會直接拋出BeanCurrentlyInCreationException
異常。
賦值屬性循環(huán)依賴,Spring只支持bean在單例模式下的循環(huán)依賴,其他模式下的循環(huán)依賴Spring也是會拋出BeanCurrentlyInCreationException
異常的。Spring通過對還在創(chuàng)建過程中的單例bean,進(jìn)行緩存并提前暴露該單例,使得其他實例可以提前引用到該單例bean。
Spring為什么只支持單例模式下的bean的賦值情況下的循環(huán)依賴
在prototype的模式下的bean,使用了一個ThreadLocal變量prototypesCurrentlyInCreation
來記錄當(dāng)前線程正在創(chuàng)建中的bean,這個變量在AbtractBeanFactory
類里。在創(chuàng)建前用beanName記錄bean,在創(chuàng)建完成后刪除bean。在prototypesCurrentlyInCreation
里采用了一個Set對象來存儲正在創(chuàng)建中的bean。我們都知道Set是不允許存在重復(fù)對象的,這樣就能保證同一個bean在一個線程中只能有一個正在創(chuàng)建。
下面是prototypesCurrentlyInCreation
變量在刪除bean時的操作,在AbtractBeanFactory
的beforePrototypeCreation
操作里。
protected void afterPrototypeCreation(String beanName) { Object curVal = this.prototypesCurrentlyInCreation.get(); if (curVal instanceof String) { this.prototypesCurrentlyInCreation.remove(); } else if (curVal instanceof Set) { Set<String> beanNameSet = (Set<String>) curVal; beanNameSet.remove(beanName); if (beanNameSet.isEmpty()) { this.prototypesCurrentlyInCreation.remove(); } } }
從上面的代碼中看出,當(dāng)變量為一個的時候采用了一個String對象來存儲,節(jié)省了一些內(nèi)存空間。
在AbstractBeanFactory
類的doGetBean
方法里先判斷是否為單例對象,不是單例對象,則直接判斷當(dāng)前線程是否已經(jīng)存在了正在創(chuàng)建的bean。存在的話直接拋出異常。
這個isPrototypeCurrentlyInCreation()
方法的實現(xiàn)代碼如下:
protected boolean isPrototypeCurrentlyInCreation(String beanName) { Object curVal = this.prototypesCurrentlyInCreation.get(); return curVal != null && (curVal.equals(beanName) || curVal instanceof Set && ((Set)curVal).contains(beanName)); }
因為有了這個機(jī)制,spring在原型模式下是解決不了bean的循環(huán)依賴的,當(dāng)發(fā)現(xiàn)有循環(huán)依賴的時候會直接拋出BeanCurrentlyInCreationException
異常的。
那么為什么spring在單例模式下的構(gòu)造賦值也不支持循環(huán)依賴呢?
其實原理和原型模式下的情況類似,在單例模式下,bean也會用一個Set集合來保存正在創(chuàng)建中的bean,在創(chuàng)建前保存,創(chuàng)建完成后刪除。
這個對象在DefaultSingletonBeanRegistry
類下變量名為:singletonsCurrentlyInCreation
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap(16)); }
判定代碼在DefaultSingletonBeanRegistry
類的beforeSingletonCreation
方法下。
protected void beforeSingletonCreation(String beanName) { if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } }
在上面這個方法中,判定singletonsCurrentlyInCreation
是否能成功的保存一個單例bean。如果不能成功保存,那么就會直接拋出BeanCurrentlyInCreationException
異常。
單例模式下的Setter賦值循環(huán)依賴
終于到了我們的重點,Spring是如何解決單例模式下的Setter賦值的循環(huán)依賴了。
其實主要的就是靠提前暴露創(chuàng)建中的單例實例。
那么具體是一個怎樣的過程呢?
例如:上面那個圖的例子,A依賴B,B依賴C,C又依賴B。
過程如下:
創(chuàng)建A,調(diào)用構(gòu)造方法,完成構(gòu)造,進(jìn)行屬性賦值注入,發(fā)現(xiàn)依賴B,去實例化B
。創(chuàng)建B,調(diào)用構(gòu)造方法,完成構(gòu)造,進(jìn)行屬性賦值注入,發(fā)現(xiàn)依賴C,去實例化C
。
創(chuàng)建C,調(diào)用構(gòu)造方法,完成構(gòu)造,進(jìn)行屬性賦值注入,發(fā)現(xiàn)依賴A。
這個時候就是解決循環(huán)依賴的關(guān)鍵了,因為A已經(jīng)通過構(gòu)造方法已經(jīng)構(gòu)造完成了,也就是說已經(jīng)將Bean的在堆中分配好了內(nèi)存,這樣即使A再填充屬性值也不會更改內(nèi)存地址了,所以此時可以提前拿出來A的引用,來完成C的實例化。
這樣上面創(chuàng)建C過程就會變成了:
創(chuàng)建C,調(diào)用構(gòu)造方法,完成構(gòu)造,進(jìn)行屬性賦值注入,發(fā)現(xiàn)依賴A,A已經(jīng)構(gòu)造完成,直接引用,完成C的實例化
。C完成實例化后,注入B,B也完成了實例化,然后B注入A,A也完成了實例化
。
為了能獲取到創(chuàng)建中單例bean,spring提供了三級緩存來將正在創(chuàng)建中的bean提前暴露。
在類DefaultSingletonBeanRegistry
下,即下圖紅框中的三個Map對象。
這三個緩存Map的作用如下:
- 一級緩存,
singletonObjects
單例緩存,存儲已經(jīng)實例化的單例bean。 - 二級緩存,
earlySingletonObjects
提前暴露的單例緩存,這里存儲的bean是剛剛構(gòu)造完成,但還會通過屬性注入bean。 - 三級緩存,
singletonFactories
生產(chǎn)單例的工廠緩存,存儲工廠。
首先在創(chuàng)建bean的時候會先創(chuàng)建一個和bean同名的單例工廠,并將bean先放入到單例工廠中。代碼在AbstractAutowireCapableBeanFactory
類的doCreateBean
方法中。
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException { ...... this.addSingletonFactory(beanName, new ObjectFactory<Object>() { public Object getObject() throws BeansException { return AbstractAutowireCapableBeanFactory.this.getEarlyBeanReference(beanName, mbd, bean); } }); ..... }
而上面的代碼中的addSingletonFactory
方法的代碼如下:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); Map var3 = this.singletonObjects; synchronized(this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } }
addSingletonFactory
方法的作用通過代碼就可以看到是將存在了正在創(chuàng)建中的bean的單例工廠,放在三級緩存里,這樣保證了在循環(huán)依賴查找的時候是可以找到bean的引用的。
具體讀取緩存獲取bean的過程在類DefaultSingletonBeanRegistry
的getSingleton
方法里。
如下源碼:
protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) { Map var4 = this.singletonObjects; synchronized(this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject != NULL_OBJECT ? singletonObject : null; }
通過上面的源碼我們可以看到,在獲取單例Bean的時候,會先從一級緩存singletonObjects
里獲取,如果沒有獲取到(說明不存在或沒有實例化完成),會去第二級緩存earlySingletonObjects
中去找,如果還是沒有找到的話,就會三級緩存中獲取單例工廠singletonFactory
,通過從singletonFactory
中獲取正在創(chuàng)建中的引用,將singletonFactory
存儲在earlySingletonObjects
二級緩存中,這樣就將創(chuàng)建中的單例引用從三級緩存中升級到了二級緩存中,二級緩存earlySingletonObjects
,是會提前暴露已完成構(gòu)造,還可以執(zhí)行屬性注入的單例bean的。
這個時候如何還有其他的bean也是需要屬性注入,那么就可以直接從earlySingletonObjects
中獲取了。
上面的例子中的過程中的A,在注入C的時候,其實并沒有真正的初始化完成,等到順利的注入了B才算是真正的初始化完成。
整個過程如下圖:
總結(jié)
到此這篇關(guān)于Spring如何解決循環(huán)依賴的問題的文章就介紹到這了,更多相關(guān)Spring解決循環(huán)依賴內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java之map的常見用法講解與五種循環(huán)遍歷實例代碼理解
map是一組鍵值對的組合,通俗理解類似一種特殊的數(shù)組,a[key]=val,只不過數(shù)組元素的下標(biāo)是任意一種類型,而且數(shù)組的元素的值也是任意一種類型。有點類似python中的字典。通過"鍵"來取值,類似生活中的字典,已知索引,來查看對應(yīng)的信息2021-09-09SpringBoot+Docker+IDEA實現(xiàn)一鍵構(gòu)建+推送、運行、同鏡像多容器啟動
這篇文章主要介紹了SpringBoot+Docker+IDEA實現(xiàn)一鍵構(gòu)建+推送、運行、同鏡像多容器啟動,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Springboot 在普通類型注入Service或mapper
這篇文章主要介紹了Springboot 在普通類型注入Service或mapper,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11如何通過Java監(jiān)聽MySQL數(shù)據(jù)的變化
對于二次開發(fā)來說,很大一部分就找找文件和找數(shù)據(jù)庫的變化情況,下面這篇文章主要給大家介紹了關(guān)于如何通過Java監(jiān)聽MySQL數(shù)據(jù)的變化的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03總結(jié)Java常用的時間相關(guān)轉(zhuǎn)化
今天給大家?guī)淼氖顷P(guān)于Java的相關(guān)知識,文章圍繞著Java常用的時間相關(guān)轉(zhuǎn)化展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06