天天看點

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

開心一刻

  一天,侄子和我哥聊天,我坐在旁邊聽着

  侄子:爸爸,你愛我媽媽嗎?

  哥:這話說的,不愛能有你嗎?

  侄子:确定有我不是因為荷爾蒙嗎?

  哥:因為什麼荷爾蒙,因為愛情!

  侄子:那我媽花點錢,你咋老說呢?

  哥:這你就不懂了,掙錢本不易,花錢要仔細

  侄子:快得了吧,掙錢這麼少,我媽都沒跑,給你照顧家,錢還不讓花

  哥:我發現你這孩子怎麼不知道好賴呢,我攢錢不是為了給你去媳婦啊

  侄子:那你趕緊給我媽花吧,我媽要是跑了,你還得花錢娶一個,到最後,錢我撈不着,親媽還混沒了

  我:通透!!!

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

寫在前面

  Spring 中常見的循環依賴有 3 種:單例 setter 循環依賴、單例構造方法循環依賴、原型循環依賴

  關于單例 setter 循環依賴,Spring 是如何甄别和處理的,可檢視:Spring 的循環依賴,源碼詳細分析 → 真的非要三級緩存嗎

  單例構造方法循環依賴

  何謂單例構造方法循環依賴了,我們看具體代碼就明白了

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  兩個要素:① scope 是預設值,也就是 singleton;② 多個執行個體之間通過構造方法形成了循環依賴

  這種情況下,Spring 是怎麼處理的了,我們先來看看執行結果

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  Spring 啟動過程中報錯了: Error creating bean with name 'cat': Requested bean is currently in creation: Is there an unresolvable circular reference? 

  問題就來了:Spring 是如何甄别單例情況下的構造方法循環依賴的,然後進行報錯的

  大家先把這個問題暫留在心裡,我們再來看看什麼是原型循環依賴

  原型循環依賴

  同樣,我們直接看代碼就明白何謂原型循環依賴了

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  同樣是 2 個要素:① scope 不是預設值,而是 prototype,也就是原型,每次擷取該執行個體的時候都會建立;② setter 循環依賴

  這種情況下 Spring 又會有什麼樣的執行結果了

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  Spring 啟動正常,但從 Spring 容器擷取 loop 執行個體的時候,報了同樣的錯誤

  問題來了:① Spring 是如何甄别原型循環依賴的,然後進行報錯提示的

       ② 為什麼兩種情況的報錯時機不一緻,一個在 Spring 啟動過程中,一個卻在使用 Spring 的過程中

  示例代碼位址:spring-circle-dependence-type

  上面的 3 個問題,概括下就是

    1、Spring 是如何甄别單例情況下的構造方法循環依賴的

    2、Spring 是如何甄别原型循環依賴的

    3、為什麼單例構造方法循環依賴和原型循環依賴的報錯時機不一緻

  我們慢慢往下看,跟源碼的過程可能比較快,大家看仔細了

  還是那句話

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  看完之後仍有疑問,可以評論區留言,也可以自行去查閱相關資料進行解疑

  源碼起點

    Spring 讀取和解析 xml 的過程,我們就不去跟了,我們重點跟一下我們關注的内容

    我們從 DefaultListableBeanFactory 類的 preInstantiateSingletons 方法作為起點

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

    按如下順序可以快速的找到起點,後面兩種情況都從此處開始進行源碼跟蹤

構造方法循環依賴的甄别

  閑話少說,我們直接開始跟源碼

  擷取 cat 執行個體

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

   cat 的 RootBeanDefinition 中有幾個屬性值得我們注意下

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  接着往下走

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  我們來到了 createBeanInstance 方法,此時 Set<String> singletonsCurrentlyInCreation 隻存放了 cat 

   singletonsCurrentlyInCreation 看字面意思就知道,存放的是目前正在建立中的單例對象名

  我們接着往下跟

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  由于 constructorArgumentValues 中有元素,是以需要通過有參構造函數來建立 cat 對象

  因為構造函數的參數是 Dog 類型的 dog ,是以通過反射調用 Cat 的有參構造函數來建立 cat 之前,需要先從 Spring 容器中擷取到 dog 對象

  擷取 Cat 構造函數依賴的 dog 執行個體

  是以流程又來到了我們熟悉的 getBean ,隻是現在擷取的是 dog ;擷取流程與擷取 cat 時一樣,是以跟的速度會快一些,大家注意看我停頓的地方

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  此時 singletonsCurrentlyInCreation 存放了 cat 和 dog ,表示他們都在建立中

  又來到了 createBeanInstance ,過程與之前 cat 的過程一樣,我們接着往下看

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  又來到了熟悉的 getBean ,需要從 Spring 容器擷取 Dog 構造函數依賴的 cat 對象

  擷取 Dog 構造函數依賴的 cat 對象

  接下來重點來了,大家看清楚了

  因為 singletonsCurrentlyInCreation 已經存在 cat 了, !this.singletonsCurrentlyInCreation.add(beanName) 結果就是 true 

  說明陷入死循環了,是以抛出了 BeanCurrentlyInCreationException 

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  我們在控制台看到的異常資訊就從這來的

原型循環依賴的甄别

  原型類型的執行個體有個特點:每次擷取都會重新建立一個執行個體,那在 Spring 啟動過程中,還有建立的必要嗎?

  Spring 啟動不建立 prototype 類型的執行個體

  我們來跟下源碼就明白了

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  關鍵代碼

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  不符合上述 3 個條件的執行個體,在 Spring 啟動過程中都不會被建立

  下面接着講正題,來看看 Spring 是如何甄别原型循環依賴的

  擷取 loop 執行個體

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  在 loop 執行個體建立之前,調用了 beforePrototypeCreation 方法,将 loop 名放到了 ThreadLocal<Object> prototypesCurrentlyInCreation 

  表示目前線程正在建立 loop ,我們接着往下看

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  原型類型的對象建立過程分兩步:① 執行個體化(反射調構造方法),② 初始化(屬性填充),和單例類型對象的建立過程是一樣的

  依賴的處理是在初始化過程中進行的, loop 對象依賴 circle 屬性,是以對 loop 對象的 circle 屬性進行填充的時候,需要去 Spring 容器擷取 circle 執行個體

  又來到了我們熟悉的 getBean ,擷取 loop 依賴的 circle 執行個體,我們繼續往下跟

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  在 circle 對象建立之前,同樣調用了 beforePrototypeCreation 方法,那麼此時 prototypesCurrentlyInCreation 中就同時存在 loop 和 circle 

  表示目前線程正在建立 loop 執行個體和 circle 執行個體;繼續往下走

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  兜兜轉轉又來到了 getBean ,擷取 circle 對象依賴的 loop 屬性,接下來是重點,大家看仔細了

再探循環依賴 → Spring 是如何判定原型循環依賴和構造方法循環依賴的?

  因為 prototypesCurrentlyInCreation 中存在 loop 了,說明目前線程正在建立 loop 執行個體

  而現在又要建立新的 loop ,說明陷入死循環了,是以抛出了 BeanCurrentlyInCreationException 

總結

  經過上面的梳理,相信大家對之前的三個問題都沒有疑問了,我們來總結下

  1、Spring 是如何甄别單例情況下的構造方法循環依賴的

    Spring 通過 Set<String> singletonsCurrentlyInCreation 記錄目前正在建立中的執行個體名稱

    建立執行個體對象之前,會判斷 singletonsCurrentlyInCreation 中是否存在該執行個體的名稱,如果存在則表示死循環了,那麼抛出 BeanCurrentlyInCreationException 

  2、Spring 是如何甄别原型循環依賴的

    Spring 通過 ThreadLocal<Object> prototypesCurrentlyInCreation 記錄目前線程正在建立中的原型執行個體名稱

    建立原型執行個體之前,會判斷 prototypesCurrentlyInCreation 中是否存在該執行個體的名稱,如果存在則表示死循環了,那麼抛出 BeanCurrentlyInCreationException 

  3、為什麼單例構造方法循環依賴和原型循環依賴的報錯時機不一緻

    單例構造方法執行個體的建立是在 Spring 啟動過程中完成的,而原型執行個體是在擷取的時候建立的

    是以兩者的循環依賴的報錯時機不一緻

參考

  Spring 的循環依賴,源碼詳細分析 → 真的非要三級緩存嗎