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

開心一刻

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

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

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

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

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

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

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

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

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

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

  我:通透!!!

寫在前面

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

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

  單例構造方法循環依賴

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

  兩個要素:① scope 是默認值,也就是 singleton;② 多個實例之間通過構造方法形成了循環依賴

  這種情況下,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 容器獲取 loop 實例的時候,報了同樣的錯誤

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

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

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

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

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

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

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

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

  還是那句話

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

  源碼起點

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

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

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

構造方法循環依賴的甄別

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

  獲取 cat 實例

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

  接着往下走

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

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

  我們接着往下跟

  由於 constructorArgumentValues 中有元素,所以需要通過有參構造函數來創建 cat 對象

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

  獲取 Cat 構造函數依賴的 dog 實例

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

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

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

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

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

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

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

  說明陷入死循環了,所以拋出了 BeanCurrentlyInCreationException 

  我們在控制檯看到的異常信息就從這來的

原型循環依賴的甄別

  原型類型的實例有個特點:每次獲取都會重新創建一個實例,那在 Spring 啓動過程中,還有創建的必要嗎?

  Spring 啓動不創建 prototype 類型的實例

  我們來跟下源碼就明白了

  關鍵代碼

  不符合上述 3 個條件的實例,在 Spring 啓動過程中都不會被創建

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

  獲取 loop 實例

  在 loop 實例創建之前,調用了 beforePrototypeCreation 方法,將 loop 名放到了 ThreadLocal<Object> prototypesCurrentlyInCreation 

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

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

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

  又來到了我們熟悉的 getBean ,獲取 loop 依賴的 circle 實例,我們繼續往下跟

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

  表示當前線程正在創建 loop 實例和 circle 實例;繼續往下走

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

  因爲 prototypesCurrentlyInCreation 中存在 loop 了,說明當前線程正在創建 loop 實例

  而現在又要創建新的 loop ,說明陷入死循環了,所以拋出了 BeanCurrentlyInCreationException 

總結

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

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

    Spring 通過 Set<String> singletonsCurrentlyInCreation 記錄當前正在創建中的實例名稱

    創建實例對象之前,會判斷 singletonsCurrentlyInCreation 中是否存在該實例的名稱,如果存在則表示死循環了,那麼拋出 BeanCurrentlyInCreationException 

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

    Spring 通過 ThreadLocal<Object> prototypesCurrentlyInCreation 記錄當前線程正在創建中的原型實例名稱

    創建原型實例之前,會判斷 prototypesCurrentlyInCreation 中是否存在該實例的名稱,如果存在則表示死循環了,那麼拋出 BeanCurrentlyInCreationException 

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

    單例構造方法實例的創建是在 Spring 啓動過程中完成的,而原型實例是在獲取的時候創建的

    所以兩者的循環依賴的報錯時機不一致

參考

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章