首頁 > 軟體

Spring詳細講解迴圈依賴是什麼

2022-08-15 18:04:11

前言

Spring在我們實際開發過程中真的太重要了,當你在公司做架構升級、沉澱工具等都會多多少少用到Spring,本人也一樣,在研習了好幾遍Spring原始碼之後,產生一系列的問題, 也從網上翻閱了各種資料,最近說服了自己,覺得還是得整理一下,有興趣的朋友可以一起討論溝通一波。回到標題,我們要知道以下幾點:

(1)什麼是迴圈依賴?

(2)Spring如何解決迴圈依賴

(3)只用一級快取會存在什麼問題

(4)只用二級快取會存在什麼問題

(5)Spring 為什麼不用二級快取來解決迴圈依賴問題

什麼是迴圈依賴

很直接的一張圖:

迴圈依賴分為三種:構造器注入方式的迴圈依賴、setter注入方式的迴圈、屬性注入方式的迴圈依賴;

其中構造器注入方式造成的迴圈依賴Spring無法解決,這一點可以通過偵錯Spring原始碼得到結論。

(ps:X和Y都是構造器注入彼此,嚴謹一點)

Spring如何處理的迴圈依賴

Spring通過三級快取解決了迴圈依賴,其中一級快取為單例池(singletonObjects),二級快取為早期曝光物件earlySingletonObjects,三級快取為早期曝光物件工廠(singletonFactories)。

當A、B兩個類發生迴圈參照時,在A完成範例化後,就使用範例化後的物件去建立一個物件工廠,並新增到三級快取中,如果A被AOP代理,那麼通過這個工廠獲取到的就是A代理後的物件,如果A沒有被AOP代理,那麼這個工廠獲取到的就是A範例化的物件。當A進行屬性注入時,會去建立B,同時B又依賴了A,所以建立B的同時又會去呼叫getBean(a)來獲取需要的依賴,此時的getBean(a)會從快取中獲取,第一步,先獲取到三級快取中的工廠;第二步,呼叫物件工工廠的getObject方法來獲取到對應的物件,得到這個物件後將其注入到B中。緊接著B會走完它的生命週期流程,包括初始化、後置處理器等。當B建立完後,會將B再注入到A中,此時A再完成它的整個生命週期。至此,迴圈依賴結束,上原始碼:

第一次建立A時

截圖處程式碼不成立,直接返回,進行A的建立過程

真正觸發去建立A的地方

這裡將A加入正在建立的一個集合,代表A正在建立當中

暴露早期A的半成品物件,如果A被AOP代理,那就是暴露A的代理物件,將改物件的建立工廠新增至三級快取中,那麼什麼會去用呢?

進行屬性注入,在這裡發現需要注入B,然後進行B的建立

B的建立同A一樣,此時就不截圖了。

然後在進行B的屬性注入時,發現需要注入A,再次回到A的建立,回到第一次建立的那個地方:

從三級快取中獲取A的半成品物件,新增至二級快取中。

此時將這個半成品物件給到B,讓B完成初始化,然後再次回到A的建立過程,上圖中觸發createBean的那個地方:

生成最終的A物件,放入一級快取中。

只用一級快取會存在什麼問題

只使用一級快取,也就是將所有的 bean 的範例都放在同一個 Map 容器中。其中就包括已經初始化好的 bean 和未初始化好的 bean。

已經初始化好的 bean: 指經過了 bean 建立的三個階段之後的 bean 物件

未初始化好的 bean : 指經過了 bean 建立的第一個階段,只將 bean 範例建立出來了

bean 的建立過程分三個階段:

1、建立範例 createBeanInstance

2、填充依賴 populateBean

3、initializeBean

假設 bean 是需要 AOP 增強的,那麼最終放到快取中的應該是一個代理 bean。而代理 bean 的產生是在 initializeBean(第三階段) 的時候。所以,我們推匯出:如果只使用一級快取的話,快取的插入應該放在 initializeBean 之後。

如果在 initializeBean 的時候記錄快取,那麼碰到迴圈依賴的情況,需要在 populateBean(第二階段) 的時候再去注入迴圈依賴的 bean,此時,快取中是沒有迴圈依賴的 bean 的,就會導致 bean 重新建立範例。

這樣顯然是不行的。

反例:迴圈依賴場景:A–>B–>A

A 在 createBeanInstance 時,建立了 bean 範例,接著 populateBean 會填充依賴的 bean B,從而觸發 B 的載入;

B 在 populateBean 時,發現要注入依賴的 bean A,先從快取中獲取 bean A,獲取不到,就會重新建立 bean A。

這樣違背了 bean 的單例性,所以只使用一級快取是不行的。

理論上使用一級快取是可以解決普通場景下的迴圈依賴的,因為對於普通場景,從始至終 bean 的物件參照始終都是不變的。

但是,如果被迴圈依賴的 bean 是一個 AOP 增強的代理 bean 的話,bean 的原始參照和最終產生的 AOP 增強 bean 的參照是不一樣的,一級快取就搞不定了。

疑問:如果在 createBeanInstance 之後就生成代理物件放入一級快取呢?

我們或許會有疑問,如果不按 spring 原本的設計,我們在 bean 建立的第一步createBeanInstance 之後就判斷是否生成代理物件,並將要暴露的物件放入一級快取是不是就可以解決所有場景的迴圈依賴問題呢?

分析:

再利用上面的反例來分析一遍,放入一級快取的 bean 與暴露到容器中的 bean (不管是否有代理)始終是同一個 bean,看似好像是沒有問題的,好像是可以解決迴圈依賴的問題。

但是這裡忽略了一個問題:bean 建立的第二步中 populateBean 的底層實現是將原始 bean 物件包裝成 BeanWrapper,然後通過 BeanWrapper 利用反射設定值的。

如果在 populateBean 之前生成的是一個代理物件的話,就帶來了另外一個問題 :jdk proxy 產生的代理物件是實現的目標類的介面,jdk proxy 的代理類通過 BeanWrapper 去利用反射設定值時會因為找不到相應的屬性或者方法而報錯。

所以,如果在 createBeanInstance 之後就生成代理物件放入一級快取,也是行不通的。

只用二級快取會存在什麼問題

使用二級快取也可以分為兩種:使用singletonObjects和earlySingletonObjects,或者使用singletonObjects和singletonFactories。

①使用singletonObjects和earlySingletonObjects

成品放在singletonObjects中,半成品放在earlySingletonObjects中

流程可以這樣走:範例化A ->將半成品的A放入earlySingletonObjects中 ->填充A的屬性時發現取不到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 ->建立A的物件工廠並放入singletonFactories中 ->填充A的屬性時發現取不到B->範例化B->建立B的物件工廠並放入singletonFactories中->從singletonFactories中獲取A的物件工廠並獲取A填充到B中->將成品B放入singletonObjects,並從singletonFactories中刪除B的物件工廠->將B填充到A的屬性中->將成品A放入singletonObjects並刪除A的物件工廠。

同樣,這樣的流程也適用於普通的IOC已經有並行的場景,但如果A上加個切面(AOP)的話,這種情況也無法滿足需求,因為當A和多個物件發生迴圈依賴時,其他物件拿到的都是A的不同的代理物件。

疑問:如果在createBeanInstance之後就生成代理物件放入二級快取呢?

我們思考一種簡單的情況,就以單獨建立A為例,假設AB之間現在沒有依賴關係,但是A被代理了,這個時候當A完成範例化後還是會進入下面這段程式碼:

    // A是單例的,mbd.isSingleton()條件滿足
    // allowCircularReferences:這個變數代表是否允許迴圈依賴,預設是開啟的,條件也滿足
    // isSingletonCurrentlyInCreation:正在在建立A,也滿足
    boolean earlySingletonExposure = (mbd.isSingleton() &&this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
    // 所以earlySingletonExposure=true
    // 還是會進入到這段程式碼中
    if(earlySingletonExposure) {
    // 還是會通過三級快取提前暴露一個工廠物件
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

看到了吧,即使沒有迴圈依賴,也會將其新增到三級快取中,而且是不得不新增到三級快取中,因為到目前為止Spring也不能確定這個Bean有沒有跟別的Bean出現迴圈依賴。

假設我們在這裡直接使用二級快取的話,那麼意味著所有的Bean在這一步都要完成AOP代理。這樣做有必要嗎?

不僅沒有必要,而且違背了Spring在結合AOP跟Bean的生命週期的設計!Spring結合AOP跟Bean的生命週期本身就是通過AnnotationAwareAspectJAutoProxyCreator這個後置處理器來完成的,在這個後置處理的postProcessAfterInitialization方法中對初始化後的Bean完成AOP代理。如果出現了迴圈依賴,那沒有辦法,只有給Bean先建立代理,但是沒有出現迴圈依賴的情況下,設計之初就是讓Bean在生命週期的最後一步完成代理而不是在範例化後就立馬完成代理。

Spring 為什麼不用二級快取來解決迴圈依賴問題

Spring 原本的設計是,bean 的建立過程分三個階段:

1 建立範例 createBeanInstance – 建立出 bean 的原始物件

2 填充依賴 populateBean – 利用反射,使用 BeanWrapper 來設定屬性值

3 initializeBean – 執行 bean 建立後的處理,包括 AOP 物件的產生

在沒有迴圈依賴的場景下:第 1,2 步都是 bean 的原始物件,第 3 步 initializeBean 時,才會生成 AOP 代理物件。

迴圈依賴屬於一個特殊的場景,如果在第 3 步 initializeBean 時才去生成 AOP 代理 bean 的話,那麼在第 2 步 populateBean 注入迴圈依賴 bean 時就拿不到 AOP 代理 bean 進行注入。

所以,迴圈依賴打破了 AOP 代理 bean 生成的時機,需要在 populateBean 之前就生成 AOP 代理 bean。

而且,生成 AOP 代理需要執行 BeanPostProcessor,而 Spring 原本的設計是在第 3 步 initializeBean 時才去呼叫 BeanPostProcessor 的。

並不是每個 bean 都需要進行這樣的處理,所以, Spring 沒有直接在 createBeanInstance 之後直接生成 bean 的早期參照,而是將 bean 的原始物件包裝成了一個 ObjectFactory 放到了三級快取 Map<String, Object> earlySingletonObjects。

當需要用到 bean 的早期參照的時候,才通過三級快取 Map<String, ObjectFactory<?>> singletonFactories 來進行獲取。

如果只使用二級快取來解決迴圈依賴的話,那麼每個 bean 的建立流程中都需要插入一個流程——建立 bean 的早期參照放入二級快取。

其實,在真實的開發中,絕大部分的情況下都不涉及到迴圈依賴,而且 createBeanInstance --> populateBean --> initializeBean 這個流程也更加符合常理。

所以,猜想 Spring 不用二級快取來解決迴圈依賴問題,是為了保證處理時清晰明瞭,bean 的建立就是三個階段: createBeanInstance --> populateBean --> initializeBean

只有碰到 AOP 代理 bean 被迴圈依賴時的場景,才去特殊處理,提前生成 AOP 代理 bean。

總結

如果沒有迴圈依賴的情況的話,一級快取就可以搞定所有的情況,只需要在 bean 完全初始化好之後將其放入一級快取即可。

但是一級快取解決不了迴圈依賴的情況,所以,Spring 使用三級快取來解決了迴圈依賴問題。如果使用二級快取的話,理論上是可行的,但是 Spring 選擇了三級快取來實現,讓 bean 的建立流程更加符合常理,更加清晰明瞭。

到此這篇關於Spring詳細講解迴圈依賴是什麼的文章就介紹到這了,更多相關Spring迴圈依賴內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com