Spring 的迴圈依賴,原始碼詳細分析 → 真的非要三級快取嗎

青石路發表於2021-02-07

開心一刻

  吃完晚飯,坐在院子裡和父親聊天

  父親:你有什麼人生追求?

  我:金錢和美女

  父親對著我的頭就是一丁弓,說道:小小年紀,怎麼這麼庸俗,重說一次

  我:事業與愛情

  父親讚賞的摸了我的頭,說道:嗯嗯,這就對咯

寫作背景

  做 Java 開發的,一般都繞不開 Spring,那麼面試中肯定會被問到 Spring 的相關內容,而迴圈依賴又是 Spring 中的高頻面試題

  這不前段時間,我的一朋友去面試,就被問到了迴圈依賴,結果他還在上面還小磕了一下,他們聊天過程如下

  面試官:說下什麼是迴圈依賴

  朋友: 兩個或則兩個以上的物件互相依賴對方,最終形成 閉環 。例如 A 物件依賴 B 物件,B 物件也依賴 A 物件

  面試官:那會有什麼問題呢

  朋友:物件的建立過程會產生死迴圈,類似如下

  面試官:Spring 是如何解決的呢

  朋友:通過三級快取提前暴露物件來解決的

  面試官:三級快取裡面分別存的什麼

  朋友:一級快取裡存的是成品物件,例項化和初始化都完成了,我們的應用中使用的物件就是一級快取中的

    二級快取中存的是半成品,用來解決物件建立過程中的迴圈依賴問題

    三級快取中存的是 ObjectFactory<?> 型別的 lambda 表示式,用於處理存在 AOP 時的迴圈依賴問題

  面試官:為什麼要用三級快取來解決迴圈依賴問題(只用一級快取行不行,只用二級快取行不行)

  朋友:霸點蠻,只用一級快取也是可以解決的,但是會複雜化整個邏輯

    半成品物件是沒法直接使用的(存在 NPE 問題),所以 Spring 需要保證在啟動的過程中,所有中間產生的半成品物件最終都會變成成品物件

    如果將半成品物件和成品物件都混在一級快取中,那麼為了區分他們,勢必會增加一些而外的標記和邏輯處理,這就會導致物件的建立過程變得複雜化了

    將半成品物件與成品物件分開存放,兩級快取各司其職,能夠簡化物件的建立過程,更簡單、直觀

    如果 Spring 不引入 AOP,那麼兩級快取就夠了,但是作為 Spring 的核心之一,AOP 怎能少得了呢

    所以為了處理 AOP 時的迴圈依賴,Spring 引入第三級快取來處理迴圈依賴時的代理物件的建立

  面試官:如果將代理物件的建立過程提前,緊隨於例項化之後,而在初始化之前,那是不是就可以只用兩級快取了?

  朋友心想:這到了我知識盲區了呀,我幹哦! 卻點頭道:你說的有道理耶,我沒有細想這一點,回頭我去改改原始碼試試看

  前面幾問,感覺朋友答的還不錯,但是最後一問中的第三級快取的作用,回答的還差那麼一丟丟,到底那一丟丟是什麼,我們慢慢往下看

寫在前面

  正式開講之前,我們先來回顧一些內容,不然可能後面的內容看起來有點蒙(其實主要是怕你們槓我)

  物件的建立

    一般而言,物件的建立分成兩步:例項化、初始化,例項化指的是從堆中申請記憶體空間,完成 JVM 層面的物件建立,初始化指的是給屬性值賦值

    當然也可以直接通過構造方法一步完成例項化與初始化,實現物件的建立

    當然還要其他的方式,比如工廠等

  Spring 的的注入方式

    有三種:構造方法注入、setter 方法注入、介面注入

    介面注入的方式太靈活,易用性比較差,所以並未廣泛應用起來,大家知道有這麼一說就好,不要去細扣了

    構造方法注入的方式,將例項化與初始化並在一起完成,能夠快速建立一個可直接使用的物件,但它沒法處理迴圈依賴的問題,瞭解就好

    setter 方法注入的方式,是在物件例項化完成之後,再通過反射呼叫物件的 setter 方法完成屬性的賦值,能夠處理迴圈依賴的問題,是後文的基石,必須要熟悉

  Spring 三級快取的順序

    三級快取的順序是由查詢循序而來,與在類中的定義順序無關

    所以第一級快取: singletonObjects ,第二級快取: earlySingletonObjects ,第三級快取: singletonFactories 

  解決思路

    拋開 Spring,讓我們自己來實現,會如何處理迴圈依賴問題呢

    半成品雖然不能直接在應用中使用,但是在物件的建立過程中還是可以使用的嘛,就像這樣

    有入棧,有出棧,而不是一直入棧,也就解決了迴圈依賴的死迴圈問題

    Spring 是不是也是這樣實現的了,基於 5.2.12.RELEASE ,我們一起來看看 Spring 是如何解決迴圈依賴的

Spring 原始碼分析

  下面會從幾種不同的情況來進行原始碼跟蹤,如果中途有疑問,先用筆記下來,全部看完了之後還有疑問,那就請評論區留言

  沒有依賴,有 AOP

    程式碼非常簡單:spring-no-dependence

    此時, SimpleBean 物件在 Spring 中是如何建立的呢,我們一起來跟下原始碼

    接下來,我們從 DefaultListableBeanFactory 的 preInstantiateSingletons 方法開始 debug 

    沒有跟進去的方法,或者快速跳過的,我們可以先略過,重點關注跟進去了的方法和停留了的程式碼,此時有幾個屬性值中的內容值得我們留意下

    我們接著從 createBean 往下跟

    關鍵程式碼在 doCreateBean 中,其中有幾個關鍵方法的呼叫值得大家去跟下

Spring 的迴圈依賴,原始碼詳細分析  → 真的非要三級快取嗎

    此時:代理物件的建立是在物件例項化完成,並且初始化也完成之後進行的,是對一個成品物件建立代理物件

    所以此種情況下:只用一級快取就夠了,其他兩個快取可以不要

  迴圈依賴,沒有AOP

    程式碼依舊非常簡單:spring-circle-simple,此時迴圈依賴的兩個類是: Circle 和 Loop 

    物件的建立過程與前面的基本一致,只是多了迴圈依賴,少了 AOP,所以我們重點關注: populateBean 和 initializeBean 方法

    先建立的是 Circle 物件,那麼我們就從建立它的 populateBean 開始,再開始之前,我們先看看三級快取中的資料情況

    我們開始跟 populateBean ,它完成屬性的填充,與迴圈依賴有關,一定要仔細看,仔細跟

    對 circle 物件的屬性 loop 進行填充的時候,去 Spring 容器中找 loop 物件,發現沒有則進行建立,又來到了熟悉的 createBean 

    此時三級快取中的資料沒有變化,但是 Set<String> singletonsCurrentlyInCreation 中多了個 loop 

    相信到這裡大家都沒有問題,我們繼續往下看

     loop 例項化完成之後,對其屬性 circle 進行填充,去 Spring 中獲取 circle 物件,又來到了熟悉的 doGetBean 

    此時一、二級快取中都沒有 circle、loop ,而三級快取中有這兩個,我們接著往下看,重點來了,仔細看哦

    通過 getSingleton 獲取 circle 時,三級快取呼叫了 getEarlyBeanReference ,但由於沒有 AOP,所以 getEarlyBeanReference 直接返回了普通的 半成品 circle 

    然後將 半成品 circle 放到了二級快取,並將其返回,然後填充到了 loop 物件中

    此時的 loop 物件就是一個成品物件了;接著將 loop 物件返回,填充到 circle 物件中,如下如所示

    我們發現直接將 成品 loop 放到了一級快取中,二級快取自始至終都沒有過 loop ,三級快取雖說存了 loop ,但沒用到就直接 remove 了

    此時快取中的資料,相信大家都能想到了

    雖說 loop 物件已經填充到了 circle 物件中,但還有一丟丟流程沒走完,我們接著往下看

    將 成品 circle 放到了一級快取中,二級快取中的 circle 沒有用到就直接 remove 了,最後各級快取中的資料相信大家都清楚了,就不展示了

    我們回顧下這種情況下各級快取的存在感,一級快取存在感十足,二級快取可以說無存在感,三級快取有存在感(向 loop 中填充 circle 的時候有用到)

    所以此種情況下:可以減少某個快取,只需要兩級快取就夠了

  迴圈依賴 + AOP

    程式碼還是非常簡單:spring-circle-aop,在迴圈依賴的基礎上加了 AOP

    比上一種情況多了 AOP,我們來看看物件的建立過程有什麼不一樣;同樣是先建立 Circle ,在建立 Loop 

    建立過程與上一種情況大體一樣,只是有小部分割槽別,跟原始碼的時候我會在這些區別上有所停頓,其他的會跳過,大家要仔細看

    例項化 Circle ,然後填充 半成品 circle 的屬性 loop ,去 Spring 容器中獲取 loop 物件,發現沒有

    則例項化 Loop ,接著填充 半成品 loop 的屬性 circle ,去 Spring 容器中獲取 circle 物件

    這個過程與前一種情況是一致的,就直接跳過了,我們從上圖中的紅色步驟開始跟原始碼,此時三級快取中的資料如下

    注意看啦,重要的地方來了

    我們發現從第三級快取獲取 circle 的時候,呼叫了 getEarlyBeanReference 建立了 半成品 circle 的代理物件

    將 半成品 circle 的代理物件放到了第二級快取中,並將代理物件返回賦值給了 半成品 loop 的 circle 屬性 

    注意:此時是在進行 loop 的初始化,但卻把 半成品 circle 的代理物件提前建立出來了

     loop 的初始化還未完成,我們接著往下看,又是一個重點,仔細看

    在 initializeBean 方法中完成了 半成品 loop 的初始化,並在最後建立了 loop 成品 的代理物件

     loop 代理物件建立完成之後會將其放入到第一級快取中(移除第三級快取中的 loop ,第二級快取自始至終都沒有 loop )

    然後將 loop 代理物件返回並賦值給 半成品 circle 的屬性 loop ,接著進行 半成品 circle 的 initializeBean 

    因為 circle 的代理物件已經生成過了(在第二級快取中),所以不用再生成代理物件了;將第二級快取中的 circle 代理物件移到第一級快取中,並返回該代理物件

    此時各級快取中的資料情況如下(普通 circle 、 loop 物件在各自代理物件的 target 中)

    我們回顧下這種情況下各級快取的存在感,一級快取仍是存在感十足,二級快取有存在感,三級快取挺有存在感

      第三級快取提前建立 circle 代理物件,不提前建立則只能給 loop 物件的屬性 circle 賦值成 半成品 circle ,那麼 loop 物件中的 circle 物件就無 AOP 增強功能了

      第二級快取用於存放 circle 代理,用於解決迴圈依賴;也許在這個示例體現的不夠明顯,因為依賴比較簡單,依賴稍複雜一些,就能感受到了

      第一級快取存放的是對外暴露的物件,可能是代理物件,也可能是普通物件

    所以此種情況下:三級快取一個都不能少

  迴圈依賴 + AOP + 刪除第三級快取

    沒有依賴,有AOP 這種情況中,我們知道 AOP 代理物件的生成是在成品物件建立完成之後建立的,這也是 Spring 的設計原則,代理物件儘量推遲建立

    迴圈依賴 + AOP 這種情況中, circle 代理物件的生成提前了,因為必須要保證其 AOP 功能,但 loop 代理物件的生成還是遵循的 Spring 的原則

    如果我們打破這個原則,將代理物件的建立邏輯提前,那是不是就可以不用三級快取了,而只用兩級快取了呢?

    程式碼依舊簡單:spring-circle-custom,只是對 Spring 的原始碼做了非常小的改動,改動如下

    去除了第三級快取,並將代理物件的建立邏輯提前,置於例項化之後,初始化之前;我們來看下執行結果

    並沒有什麼問題,有興趣的可以去跟下原始碼,跟蹤過程相信大家已經掌握,這裡就不再演示了

  迴圈依賴 + AOP + 註解

    目前基於 xml 的配置越來越少,而基於註解的配置越來越多,所以了也提供了一個註解的版本供大家去跟原始碼

    程式碼還是很簡單:spring-circle-annotation

    跟蹤流程與 迴圈依賴 + AOP 那種情況基本一致,只是屬性的填充有了一些區別,具體可檢視:Spring 的自動裝配 → 騷話 @Autowired 的底層工作原理

總結

  1、三級快取各自的作用

    第一級快取存的是對外暴露的物件,也就是我們應用需要用到的

    第二級快取的作用是為了處理迴圈依賴的物件建立問題,裡面存的是半成品物件或半成品物件的代理物件

    第三級快取的作用處理存在 AOP + 迴圈依賴的物件建立問題,能將代理物件提前建立

  2、Spring 為什麼要引入第三級快取

    嚴格來講,第三級快取並非缺它不可,因為可以提前建立代理物件

    提前建立代理物件只是會節省那麼一丟丟記憶體空間,並不會帶來效能上的提升,但是會破環 Spring 的設計原則

    Spring 的設計原則是儘可能保證普通物件建立完成之後,再生成其 AOP 代理(儘可能延遲代理物件的生成)

    所以 Spring 用了第三級快取,既維持了設計原則,又處理了迴圈依賴;犧牲那麼一丟丟記憶體空間是願意接受的

相關文章