1. 程式人生 > >再探迴圈依賴 → Spring 是如何判定原型迴圈依賴和構造方法迴圈依賴的?

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