iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Software Development

30 篇淺談 Spring 框架的核心底層組件 系列 第 5

Day 05: DefaultSingletonBeanRegistry —— 三層快取的循環依賴解決方法

  • 分享至 

  • xImage
  •  

循環依賴的寫法在真實場景中非常常見。像「『訂單』依賴『支付』進行加扣款、『支付』依賴『訂單』進行操作回報。」為了達成上述的邏輯,在 Order-Service 依賴注入一個 Payment-Service(並在後者做相同的事情)是個很淺顯易懂的作法。今天我們就來談談 Default-Singleton-Bean-Registry(後面簡稱 Registry)看看它是怎麼解決的這個常見的棘手問題。

Registry 主要透過 3 張 Map 映射來解決循環依賴的問題。按照讀取的順序來看,這三張表分別是 singleton-Objects(後面簡稱 Cache-1,負責儲存已經建立好的 bean)、early-Singleton-Objects(後面簡稱 Cache-2,負責暫存正在創建中的 bean)、和 singleton-Factories(後面簡稱 Cache-3,負責儲存創建 bean 的 BeanFactory)。這裡可以發現 Cache-3 的長相跟其他兩個有點不一樣,我們把它拉出來談一下。

實際上 Cache-3 存儲的是「工廠用來創建半成品 Bean 的 lambda 方法。」原因後面會說,但我們可以先記憶這個方法的具體細節是「如果這個做到一半被中斷的半成品不需要 AOP 代理,那我們就直接回傳半成品;但如果它需要做 AOP 代理,我們就先把半成品包上一層 AOP 代理,然後回傳代理回去。」看起來還是不懂在幹嘛嗎?沒事,伏筆寫好了,我們接著往下看。

Registry 裡面,主要負責處理循環依賴的方法叫做 getSingleton。內部是一個有點波動拳的結構,程式會這樣寫的原因是要確保第 3 層半成品邏輯不可以重複執行。加上為了整體的運行效率,系統沒有直接對整個方法做 synchronized 處理。如果撇開所有的 if-else 判斷,它實際上在做的事情很單純,就是依序 Cache-123,並將最先有回傳的 Bean 交還給調用方。

https://ithelp.ithome.com.tw/upload/images/20260919/20184114T8hRwCt1i5.png

再來進入正題,我們用一次 A-B-A 場景說明循環依賴:

https://ithelp.ithome.com.tw/upload/images/20260919/20184114TzA77YYJnf.png

https://ithelp.ithome.com.tw/upload/images/20260919/201841140M7e0INf8I.png

  1. 第 1 階段

在實際的場景下,假設 A/B 2 個 Bean 相互依賴,且 A 先進到後續的創建邏輯。容器首先會嘗試用 getBean 查找 A:呼叫 getBean 方法、觸發 doGetBean 的方法細節:步驟 1-3 沒有成功找到(因為還沒建好)。步驟 4 就會先將 A 標記成「創建中」。然後一路走到步驟 7,觸發 A 的 create 方法,執行 Bean A 的創建細節。

  1. 第 2 階段

doCreateBean 的步驟 1 會先將 A 的空殼給 new 出來。同時在步驟 3(紀錄製作自己的 bean 工廠)這邊,就會將工廠自身儲存進 Cache-3,然後的步驟 4 是一些實例化後的相關操作,在這裡可以快速跳過;步驟 5 就會檢查 @Autowired 註解,並嘗試把 A 物件的相關依賴(也就是 Bean B)注入到 A 的成員變數裡面。所以觸發容器對 B 的 getBean。這裡就會中斷 A 的流程,遞迴進到 B 的處理細節上。

  1. 第 3 階段

B 的 getBean 整體流程跟 A 很像:步驟 1-3 沒有找到 B、步驟 4 標記 B 正在創建中、然後步驟 7 觸發 B 的 createBean,進到 doCreateBean 環節;doCreateBean 的步驟 1 實例化空殼 B、步驟 3 將製作半成品 B 的 lambda 記錄到 Cache-3,然後步驟 5 檢查 B 的 @Autowired 註解,嘗試把 A 注入到 B 的成員變數裡面,第二次遞迴呼叫 A 的 getBean。

  1. 第 4 階段

然後有趣的事情就來了:到目前為止,我們已經幫 A/B 都標記成「創建中」,且 Cache-3 都有製作半成品 A/B 的lambda 方法。所以在第二次的 get A 裡。doGetBean 的第二步(檢查是否已建立或創建中)就會被正確的捕捉到 —— 實際上,doGetBean 的 getSingleton 呼叫的就是 Registry 的 getSingleton,因此在這時候,如果我們對 Registry 進行查表,就可以找到 A 的那個 lambda 方法。

再來就是執行 lambda,把奇怪的伏筆做回收 —— 「如果這個做到一半被中斷的半成品 A 不需要 AOP 代理,那我們就直接回傳半成品 A;但如果它需要做 AOP 代理,我們就先把半成品 A 包上一層 AOP 代理,然後回傳代理回去。」實際上 Registry 在做完 Cache-3 的 lambda 之後,還會做一個小操作,會把半成品 A 丟回 Cache-2,以便後續的快速取用。

  1. 第 5 階段

然後遞迴開始收斂:第 3 階段的第二次遞迴結束了。B 成功拿到一個「半成品 A」,並將它注入到 B 的依賴上。這裡可以再提一次:B 拿到的 A 在這裡只會是兩種東西:要嘛是第 2 階段被中斷的 A,要嘛是包著一個被中斷的 A 的代理物件。接著繼續完成 doCreateBean,把一個完整的 B 做出來。最後把完成 createBean 的丟回 Cache-1,完成 Bean B 的 createBean 跟 getBean 流程,

  1. 第 6 階段

最後遞迴收斂到第 1 階段:被中斷的 A 重新被撿起來執行。這時候 A 已經可以正確拿到「注入半成品 A」的完成品 B 了。A 就會接著執行 doCreateBean 的後續流程,讓自己從半成品組裝到完的成品 A。最後就跟 B 一樣,在做完 createBean 之後,把 A 放進 Cache-1 裡面。至此,一個 A-B-A 的 @Autoriewd 循環依賴就被完整的處理完畢。

最後我們解釋一下「半成品」的 AOP 問題:在實際的場景下,如果一個 Bean 有被 AOP 進行代理。那 IoC 容器的 Bean Map 最後就會紀錄代理物件(而非原始物件)進行調用與管理。這代表如果我們在 lambda 內只做到「回傳做到一半被中斷的 A」且 A 後續又有做 AOP 代理的話,就會導致 IoC 容器管理的 A,跟 B 內部持有的 A 不是同一個 A。

此外,也因為這個原因,如果 lambda 只有執行「回傳做到一半被中斷的 A。」後續如果 B 進行 A 相關的方法調用,因為 B 持有的不是代理 A(而是原始 A),自然就不會觸發到 A 的相關切面邏輯。因此,lambda 內才會多做一個判斷,先確認 A 後續是否有切面擴充邏輯,如果沒有,當然回傳原始 A 即可;但如果有,就必須要讓 A 先做完那些切面的相關處理,才可以讓 B 參照與持有。

大概就是這樣,今天我們簡單介紹了 Spring 裡先循環依賴的方法處理細節。初次看可能會有點饒口(或至少我自己在第一次看的時候沒辦法馬上搞懂),建議如果一次看不懂的話,可以多看個 2-3 次。明天我們會承襲昨天說到的內容(創建 Bean、依賴注入、和套用 BPP),帶各位認識一下 Bean 的後製處理器,看看在創建週期裡面的初始化階段,又有什麼相關的內容與操作。

期待後續各位的閱讀與分享,我是 Pax,我們明天見。


上一篇
Day 04: BeanFactory—— Bean 的創建週期和取得方式
下一篇
Day 06: BeanPostProcessor —— Bean 初始化前後的擴充與處理機制
系列文
30 篇淺談 Spring 框架的核心底層組件 8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言