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

開心一刻

  喫完晚飯,坐在院子裏和父親聊天

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

  我:金錢和美女

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

  我:事業與愛情

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

寫作背景

  做 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 中,其中有幾個關鍵方法的調用值得大家去跟下

    此時:代理對象的創建是在對象實例化完成,並且初始化也完成之後進行的,是對一個成品對象創建代理對象

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

  循環依賴,沒有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 用了第三級緩存,既維持了設計原則,又處理了循環依賴;犧牲那麼一丟丟內存空間是願意接受的

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