Spring詳細(xì)講解循環(huán)依賴是什么
前言
Spring在我們實(shí)際開發(fā)過程中真的太重要了,當(dāng)你在公司做架構(gòu)升級、沉淀工具等都會多多少少用到Spring,本人也一樣,在研習(xí)了好幾遍Spring源碼之后,產(chǎn)生一系列的問題, 也從網(wǎng)上翻閱了各種資料,最近說服了自己,覺得還是得整理一下,有興趣的朋友可以一起討論溝通一波。回到標(biāo)題,我們要知道以下幾點(diǎn):
(1)什么是循環(huán)依賴?
(2)Spring如何解決循環(huán)依賴
(3)只用一級緩存會存在什么問題
(4)只用二級緩存會存在什么問題
(5)Spring 為什么不用二級緩存來解決循環(huán)依賴問題
什么是循環(huán)依賴
很直接的一張圖:
循環(huán)依賴分為三種:構(gòu)造器注入方式的循環(huán)依賴、setter注入方式的循環(huán)、屬性注入方式的循環(huán)依賴;
其中構(gòu)造器注入方式造成的循環(huán)依賴Spring無法解決,這一點(diǎn)可以通過調(diào)試Spring源碼得到結(jié)論。
(ps:X和Y都是構(gòu)造器注入彼此,嚴(yán)謹(jǐn)一點(diǎn))
Spring如何處理的循環(huán)依賴
Spring通過三級緩存解決了循環(huán)依賴,其中一級緩存為單例池(singletonObjects),二級緩存為早期曝光對象earlySingletonObjects,三級緩存為早期曝光對象工廠(singletonFactories)。
當(dāng)A、B兩個(gè)類發(fā)生循環(huán)引用時(shí),在A完成實(shí)例化后,就使用實(shí)例化后的對象去創(chuàng)建一個(gè)對象工廠,并添加到三級緩存中,如果A被AOP代理,那么通過這個(gè)工廠獲取到的就是A代理后的對象,如果A沒有被AOP代理,那么這個(gè)工廠獲取到的就是A實(shí)例化的對象。當(dāng)A進(jìn)行屬性注入時(shí),會去創(chuàng)建B,同時(shí)B又依賴了A,所以創(chuàng)建B的同時(shí)又會去調(diào)用getBean(a)來獲取需要的依賴,此時(shí)的getBean(a)會從緩存中獲取,第一步,先獲取到三級緩存中的工廠;第二步,調(diào)用對象工工廠的getObject方法來獲取到對應(yīng)的對象,得到這個(gè)對象后將其注入到B中。緊接著B會走完它的生命周期流程,包括初始化、后置處理器等。當(dāng)B創(chuàng)建完后,會將B再注入到A中,此時(shí)A再完成它的整個(gè)生命周期。至此,循環(huán)依賴結(jié)束,上源碼:
第一次創(chuàng)建A時(shí)
截圖處代碼不成立,直接返回,進(jìn)行A的創(chuàng)建過程
真正觸發(fā)去創(chuàng)建A的地方
這里將A加入正在創(chuàng)建的一個(gè)集合,代表A正在創(chuàng)建當(dāng)中
暴露早期A的半成品對象,如果A被AOP代理,那就是暴露A的代理對象,將改對象的創(chuàng)建工廠添加至三級緩存中,那么什么會去用呢?
進(jìn)行屬性注入,在這里發(fā)現(xiàn)需要注入B,然后進(jìn)行B的創(chuàng)建
B的創(chuàng)建同A一樣,此時(shí)就不截圖了。
然后在進(jìn)行B的屬性注入時(shí),發(fā)現(xiàn)需要注入A,再次回到A的創(chuàng)建,回到第一次創(chuàng)建的那個(gè)地方:
從三級緩存中獲取A的半成品對象,添加至二級緩存中。
此時(shí)將這個(gè)半成品對象給到B,讓B完成初始化,然后再次回到A的創(chuàng)建過程,上圖中觸發(fā)createBean的那個(gè)地方:
生成最終的A對象,放入一級緩存中。
只用一級緩存會存在什么問題
只使用一級緩存,也就是將所有的 bean 的實(shí)例都放在同一個(gè) Map 容器中。其中就包括已經(jīng)初始化好的 bean 和未初始化好的 bean。
已經(jīng)初始化好的 bean: 指經(jīng)過了 bean 創(chuàng)建的三個(gè)階段之后的 bean 對象
未初始化好的 bean : 指經(jīng)過了 bean 創(chuàng)建的第一個(gè)階段,只將 bean 實(shí)例創(chuàng)建出來了
bean 的創(chuàng)建過程分三個(gè)階段:
1、創(chuàng)建實(shí)例 createBeanInstance
2、填充依賴 populateBean
3、initializeBean
假設(shè) bean 是需要 AOP 增強(qiáng)的,那么最終放到緩存中的應(yīng)該是一個(gè)代理 bean。而代理 bean 的產(chǎn)生是在 initializeBean(第三階段) 的時(shí)候。所以,我們推導(dǎo)出:如果只使用一級緩存的話,緩存的插入應(yīng)該放在 initializeBean 之后。
如果在 initializeBean 的時(shí)候記錄緩存,那么碰到循環(huán)依賴的情況,需要在 populateBean(第二階段) 的時(shí)候再去注入循環(huán)依賴的 bean,此時(shí),緩存中是沒有循環(huán)依賴的 bean 的,就會導(dǎo)致 bean 重新創(chuàng)建實(shí)例。
這樣顯然是不行的。
反例:循環(huán)依賴場景:A–>B–>A
A 在 createBeanInstance 時(shí),創(chuàng)建了 bean 實(shí)例,接著 populateBean 會填充依賴的 bean B,從而觸發(fā) B 的加載;
B 在 populateBean 時(shí),發(fā)現(xiàn)要注入依賴的 bean A,先從緩存中獲取 bean A,獲取不到,就會重新創(chuàng)建 bean A。
這樣違背了 bean 的單例性,所以只使用一級緩存是不行的。
理論上使用一級緩存是可以解決普通場景下的循環(huán)依賴的,因?yàn)閷τ谄胀▓鼍?,從始至終 bean 的對象引用始終都是不變的。
但是,如果被循環(huán)依賴的 bean 是一個(gè) AOP 增強(qiáng)的代理 bean 的話,bean 的原始引用和最終產(chǎn)生的 AOP 增強(qiáng) bean 的引用是不一樣的,一級緩存就搞不定了。
疑問:如果在 createBeanInstance 之后就生成代理對象放入一級緩存呢?
我們或許會有疑問,如果不按 spring 原本的設(shè)計(jì),我們在 bean 創(chuàng)建的第一步createBeanInstance 之后就判斷是否生成代理對象,并將要暴露的對象放入一級緩存是不是就可以解決所有場景的循環(huán)依賴問題呢?
分析:
再利用上面的反例來分析一遍,放入一級緩存的 bean 與暴露到容器中的 bean (不管是否有代理)始終是同一個(gè) bean,看似好像是沒有問題的,好像是可以解決循環(huán)依賴的問題。
但是這里忽略了一個(gè)問題:bean 創(chuàng)建的第二步中 populateBean 的底層實(shí)現(xiàn)是將原始 bean 對象包裝成 BeanWrapper,然后通過 BeanWrapper 利用反射設(shè)置值的。
如果在 populateBean 之前生成的是一個(gè)代理對象的話,就帶來了另外一個(gè)問題 :jdk proxy 產(chǎn)生的代理對象是實(shí)現(xiàn)的目標(biāo)類的接口,jdk proxy 的代理類通過 BeanWrapper 去利用反射設(shè)置值時(shí)會因?yàn)檎也坏较鄳?yīng)的屬性或者方法而報(bào)錯(cuò)。
所以,如果在 createBeanInstance 之后就生成代理對象放入一級緩存,也是行不通的。
只用二級緩存會存在什么問題
使用二級緩存也可以分為兩種:使用singletonObjects和earlySingletonObjects,或者使用singletonObjects和singletonFactories。
①使用singletonObjects和earlySingletonObjects
成品放在singletonObjects中,半成品放在earlySingletonObjects中
流程可以這樣走:實(shí)例化A ->將半成品的A放入earlySingletonObjects中 ->填充A的屬性時(shí)發(fā)現(xiàn)取不到B->實(shí)例化B->將半成品的B放入earlySingletonObjects中->從earlySingletonObjects中取出A填充B的屬性->將成品B放入singletonObjects,并從earlySingletonObjects中刪除B->將B填充到A的屬性中->將成品A放入singletonObjects并刪除earlySingletonObjects。
這樣的流程是線程安全的,不過如果A上加個(gè)切面(AOP),這種做法就沒法滿足需求了,因?yàn)閑arlySingletonObjects中存放的都是原始對象,而我們需要注入的其實(shí)是A的代理對象。
②使用singletonObjects和singletonFactories
成品放在singletonObjects中,半成品通過singletonFactories來獲取
流程是這樣的:實(shí)例化A ->創(chuàng)建A的對象工廠并放入singletonFactories中 ->填充A的屬性時(shí)發(fā)現(xiàn)取不到B->實(shí)例化B->創(chuàng)建B的對象工廠并放入singletonFactories中->從singletonFactories中獲取A的對象工廠并獲取A填充到B中->將成品B放入singletonObjects,并從singletonFactories中刪除B的對象工廠->將B填充到A的屬性中->將成品A放入singletonObjects并刪除A的對象工廠。
同樣,這樣的流程也適用于普通的IOC已經(jīng)有并發(fā)的場景,但如果A上加個(gè)切面(AOP)的話,這種情況也無法滿足需求,因?yàn)楫?dāng)A和多個(gè)對象發(fā)生循環(huán)依賴時(shí),其他對象拿到的都是A的不同的代理對象。
疑問:如果在createBeanInstance之后就生成代理對象放入二級緩存呢?
我們思考一種簡單的情況,就以單獨(dú)創(chuàng)建A為例,假設(shè)AB之間現(xiàn)在沒有依賴關(guān)系,但是A被代理了,這個(gè)時(shí)候當(dāng)A完成實(shí)例化后還是會進(jìn)入下面這段代碼:
// A是單例的,mbd.isSingleton()條件滿足 // allowCircularReferences:這個(gè)變量代表是否允許循環(huán)依賴,默認(rèn)是開啟的,條件也滿足 // isSingletonCurrentlyInCreation:正在在創(chuàng)建A,也滿足 boolean earlySingletonExposure = (mbd.isSingleton() &&this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); // 所以earlySingletonExposure=true // 還是會進(jìn)入到這段代碼中 if(earlySingletonExposure) { // 還是會通過三級緩存提前暴露一個(gè)工廠對象 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); }
看到了吧,即使沒有循環(huán)依賴,也會將其添加到三級緩存中,而且是不得不添加到三級緩存中,因?yàn)榈侥壳盀橹筍pring也不能確定這個(gè)Bean有沒有跟別的Bean出現(xiàn)循環(huán)依賴。
假設(shè)我們在這里直接使用二級緩存的話,那么意味著所有的Bean在這一步都要完成AOP代理。這樣做有必要嗎?
不僅沒有必要,而且違背了Spring在結(jié)合AOP跟Bean的生命周期的設(shè)計(jì)!Spring結(jié)合AOP跟Bean的生命周期本身就是通過AnnotationAwareAspectJAutoProxyCreator這個(gè)后置處理器來完成的,在這個(gè)后置處理的postProcessAfterInitialization方法中對初始化后的Bean完成AOP代理。如果出現(xiàn)了循環(huán)依賴,那沒有辦法,只有給Bean先創(chuàng)建代理,但是沒有出現(xiàn)循環(huán)依賴的情況下,設(shè)計(jì)之初就是讓Bean在生命周期的最后一步完成代理而不是在實(shí)例化后就立馬完成代理。
Spring 為什么不用二級緩存來解決循環(huán)依賴問題
Spring 原本的設(shè)計(jì)是,bean 的創(chuàng)建過程分三個(gè)階段:
1 創(chuàng)建實(shí)例 createBeanInstance – 創(chuàng)建出 bean 的原始對象
2 填充依賴 populateBean – 利用反射,使用 BeanWrapper 來設(shè)置屬性值
3 initializeBean – 執(zhí)行 bean 創(chuàng)建后的處理,包括 AOP 對象的產(chǎn)生
在沒有循環(huán)依賴的場景下:第 1,2 步都是 bean 的原始對象,第 3 步 initializeBean 時(shí),才會生成 AOP 代理對象。
循環(huán)依賴屬于一個(gè)特殊的場景,如果在第 3 步 initializeBean 時(shí)才去生成 AOP 代理 bean 的話,那么在第 2 步 populateBean 注入循環(huán)依賴 bean 時(shí)就拿不到 AOP 代理 bean 進(jìn)行注入。
所以,循環(huán)依賴打破了 AOP 代理 bean 生成的時(shí)機(jī),需要在 populateBean 之前就生成 AOP 代理 bean。
而且,生成 AOP 代理需要執(zhí)行 BeanPostProcessor,而 Spring 原本的設(shè)計(jì)是在第 3 步 initializeBean 時(shí)才去調(diào)用 BeanPostProcessor 的。
并不是每個(gè) bean 都需要進(jìn)行這樣的處理,所以, Spring 沒有直接在 createBeanInstance 之后直接生成 bean 的早期引用,而是將 bean 的原始對象包裝成了一個(gè) ObjectFactory 放到了三級緩存 Map<String, Object> earlySingletonObjects。
當(dāng)需要用到 bean 的早期引用的時(shí)候,才通過三級緩存 Map<String, ObjectFactory<?>> singletonFactories 來進(jìn)行獲取。
如果只使用二級緩存來解決循環(huán)依賴的話,那么每個(gè) bean 的創(chuàng)建流程中都需要插入一個(gè)流程——創(chuàng)建 bean 的早期引用放入二級緩存。
其實(shí),在真實(shí)的開發(fā)中,絕大部分的情況下都不涉及到循環(huán)依賴,而且 createBeanInstance --> populateBean --> initializeBean 這個(gè)流程也更加符合常理。
所以,猜想 Spring 不用二級緩存來解決循環(huán)依賴問題,是為了保證處理時(shí)清晰明了,bean 的創(chuàng)建就是三個(gè)階段: createBeanInstance --> populateBean --> initializeBean
只有碰到 AOP 代理 bean 被循環(huán)依賴時(shí)的場景,才去特殊處理,提前生成 AOP 代理 bean。
總結(jié)
如果沒有循環(huán)依賴的情況的話,一級緩存就可以搞定所有的情況,只需要在 bean 完全初始化好之后將其放入一級緩存即可。
但是一級緩存解決不了循環(huán)依賴的情況,所以,Spring 使用三級緩存來解決了循環(huán)依賴問題。如果使用二級緩存的話,理論上是可行的,但是 Spring 選擇了三級緩存來實(shí)現(xiàn),讓 bean 的創(chuàng)建流程更加符合常理,更加清晰明了。
到此這篇關(guān)于Spring詳細(xì)講解循環(huán)依賴是什么的文章就介紹到這了,更多相關(guān)Spring循環(huán)依賴內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java 設(shè)計(jì)模式之單例的實(shí)例詳解
這篇文章主要介紹了java 設(shè)計(jì)模式之單例的實(shí)例詳解的相關(guān)資料, 希望通過本文能幫助到大家,讓大家徹底理解掌握單例模式,需要的朋友可以參考下2017-09-09spring boot 1.5.4 集成shiro+cas,實(shí)現(xiàn)單點(diǎn)登錄和權(quán)限控制
這篇文章主要介紹了spring boot 1.5.4 集成shiro+cas,實(shí)現(xiàn)單點(diǎn)登錄和權(quán)限控制,需要的朋友可以參考下2017-06-06java冷知識:javac AbstractProcessor詳解
這篇文章主要介紹了java冷知識:javac AbstractProcessor詳解,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Springboot開發(fā)OAuth2認(rèn)證授權(quán)與資源服務(wù)器操作
這篇文章主要介紹了Springboot開發(fā)OAuth2認(rèn)證授權(quán)與資源服務(wù)器操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Mybatis?sqlMapConfig.xml中的mappers標(biāo)簽使用
這篇文章主要介紹了Mybatis?sqlMapConfig.xml中的mappers標(biāo)簽使用方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。2022-01-01Spring security實(shí)現(xiàn)記住我下次自動登錄功能過程詳解
這篇文章主要介紹了Spring security實(shí)現(xiàn)記住我下次自動登錄功能過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03Java二維數(shù)組實(shí)現(xiàn)數(shù)字拼圖效果
這篇文章主要為大家詳細(xì)介紹了Java二維數(shù)組實(shí)現(xiàn)數(shù)字拼圖效果,控制臺可以對空格進(jìn)行移動,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07