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