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

青石路發表於2021-03-15

開心一刻

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

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

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

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

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

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

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

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

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

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

  我:通透!!!

寫在前面

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

  關於單例 setter 迴圈依賴,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 是如何甄別單例情況下的構造方法迴圈依賴的,然後進行報錯的

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

  原型迴圈依賴

  同樣,我們直接看程式碼就明白何謂原型迴圈依賴了

  同樣是 2 個要素:① scope 不是預設值,而是 prototype,也就是原型,每次獲取該例項的時候都會新建;② setter 迴圈依賴

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

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

  Spring 啟動正常,但從 Spring 容器獲取 loop 例項的時候,報了同樣的錯誤

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

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

  示例程式碼地址:spring-circle-dependence-type

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

    1、Spring 是如何甄別單例情況下的構造方法迴圈依賴的

    2、Spring 是如何甄別原型迴圈依賴的

    3、為什麼單例構造方法迴圈依賴和原型迴圈依賴的報錯時機不一致

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

  還是那句話

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

  原始碼起點

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

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

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

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

構造方法迴圈依賴的甄別

  閒話少說,我們直接開始跟原始碼

  獲取 cat 例項

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

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

  接著往下走

再探迴圈依賴 → 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 啟動不建立 prototype 型別的例項

  我們來跟下原始碼就明白了

再探迴圈依賴 → 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 的迴圈依賴,原始碼詳細分析 → 真的非要三級快取嗎

相關文章