天天看點

Spring 是如何解決循環依賴的?寫得太好了

寫作背景

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

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

面試官:說下什麼是循環依賴

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

Spring 是如何解決循環依賴的?寫得太好了

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

朋友:對象的建立過程會産生死循環,類似如下

Spring 是如何解決循環依賴的?寫得太好了

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

朋友:通過三級緩存提前暴露對象來解決的

面試官:三級緩存裡面分别存的什麼

朋友:一級緩存裡存的是成品對象,執行個體化和初始化都完成了,我們的應用中使用的對象就是一級緩存中的

二級緩存中存的是半成品,用來解決對象建立過程中的循環依賴問題

三級緩存中存的是 ObjectFactory 類型的 lambda 表達式,用于處理存在 AOP 時的循環依賴問題

Spring 是如何解決循環依賴的?寫得太好了

面試官:為什麼要用三級緩存來解決循環依賴問題(隻用一級緩存行不行,隻用二級緩存行不行)

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

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

如果将半成品對象和成品對象都混在一級緩存中,那麼為了區分他們,勢必會增加一些而外的标記和邏輯處理,這就會導緻對象的建立過程變得複雜化了

将半成品對象與成品對象分開存放,兩級緩存各司其職,能夠簡化對象的建立過程,更簡單、直覺

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

是以為了處理 AOP 時的循環依賴,Spring 引入第三級緩存來處理循環依賴時的代理對象的建立

面試官:如果将代理對象的建立過程提前,緊随于執行個體化之後,而在初始化之前,那是不是就可以隻用兩級緩存了?

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

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

寫在前面

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

1、對象的建立

一般而言,對象的建立分成兩步:執行個體化、初始化,執行個體化指的是從堆中申請記憶體空間,完成 JVM 層面的對象建立,初始化指的是給屬性值指派

當然也可以直接通過構造方法一步完成執行個體化與初始化,實作對象的建立

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

2、Spring 的的注入方式

有三種:構造方法注入、setter 方法注入、接口注入

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

構造方法注入的方式,将執行個體化與初始化并在一起完成,能夠快速建立一個可直接使用的對象,但它沒法處理循環依賴的問題,了解就好

setter 方法注入的方式,是在對象執行個體化完成之後,再通過反射調用對象的 setter 方法完成屬性的指派,能夠處理循環依賴的問題,是後文的基石,必須要熟悉

3、Spring 三級緩存的順序

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

Spring 是如何解決循環依賴的?寫得太好了

是以第一級緩存:singletonObjects ,第二級緩存:earlySingletonObjects ,第三級緩存:singletonFactories

4、解決思路

抛開 Spring,讓我們自己來實作,會如何處理循環依賴問題呢

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

Spring 是如何解決循環依賴的?寫得太好了

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

Spring 是不是也是這樣實作的了,基于 5.2.12.RELEASE ,我們一起來看看 Spring 是如何解決循環依賴的

Spring 源碼分析

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

推薦一個 Spring Boot 基礎教程及實戰示例:

https://github.com/javastacks/spring-boot-best-practice

1、沒有依賴,有 AOP

代碼非常簡單:spring-no-dependence

https://gitee.com/youzhibing/spring-circle/tree/master/spring-no-dependence

此時, SimpleBean 對象在 Spring 中是如何建立的呢,我們一起來跟下源碼

Spring 是如何解決循環依賴的?寫得太好了

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

Spring 是如何解決循環依賴的?寫得太好了

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

Spring 是如何解決循環依賴的?寫得太好了

我們接着從 createBean 往下跟

Spring 是如何解決循環依賴的?寫得太好了

關鍵代碼在 doCreateBean 中,其中有幾個關鍵方法的調用值得大家去跟下

Spring 是如何解決循環依賴的?寫得太好了

此時:代理對象的建立是在對象執行個體化完成,并且初始化也完成之後進行的,是對一個成品對象建立代理對象

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

2、循環依賴,沒有AOP

代碼依舊非常簡單:spring-circle-simple

https://gitee.com/youzhibing/spring-circle/tree/master/spring-circle-simple

此時循環依賴的兩個類是:Circle 和 Loop

對象的建立過程與前面的基本一緻,隻是多了循環依賴,少了 AOP,是以我們重點關注:populateBean 和 initializeBean 方法

先建立的是 Circle 對象,那麼我們就從建立它的 populateBean 開始,再開始之前,我們先看看三級緩存中的資料情況

Spring 是如何解決循環依賴的?寫得太好了

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

Spring 是如何解決循環依賴的?寫得太好了

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

此時三級緩存中的資料沒有變化,但是 Set singletonsCurrentlyInCreation 中多了個 loop

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

Spring 是如何解決循環依賴的?寫得太好了

loop 執行個體化完成之後,對其屬性 circle 進行填充,去 Spring 中擷取 circle 對象,又來到了熟悉的 doGetBean

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

Spring 是如何解決循環依賴的?寫得太好了

通過 getSingleton 擷取 circle 時,三級緩存調用了 getEarlyBeanReference ,但由于沒有 AOP,是以 getEarlyBeanReference 直接傳回了普通的 半成品 circle

然後将 半成品 circle 放到了二級緩存,并将其傳回,然後填充到了 loop 對象中

此時的 loop 對象就是一個成品對象了;接着将 loop 對象傳回,填充到 circle 對象中,如下如所示

Spring 是如何解決循環依賴的?寫得太好了

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

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

Spring 是如何解決循環依賴的?寫得太好了

雖說 loop 對象已經填充到了 circle 對象中,但還有一丢丢流程沒走完,我們接着往下看

Spring 是如何解決循環依賴的?寫得太好了

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

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

是以此種情況下:可以減少某個緩存,隻需要兩級緩存就夠了

3、循環依賴 + AOP

代碼還是非常簡單:spring-circle-aop,在循環依賴的基礎上加了 AOP

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

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

執行個體化 Circle ,然後填充 半成品 circle 的屬性 loop ,去 Spring 容器中擷取 loop 對象,發現沒有

則執行個體化 Loop ,接着填充 半成品 loop 的屬性 circle ,去 Spring 容器中擷取 circle 對象

Spring 是如何解決循環依賴的?寫得太好了

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

Spring 是如何解決循環依賴的?寫得太好了

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

Spring 是如何解決循環依賴的?寫得太好了

我們發現從第三級緩存擷取 circle 的時候,調用了 getEarlyBeanReference 建立了 半成品 circle 的代理對象

将 半成品 circle 的代理對象放到了第二級緩存中,并将代理對象傳回指派給了 半成品 loop 的 circle 屬性

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

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

Spring 是如何解決循環依賴的?寫得太好了

在 initializeBean 方法中完成了 半成品 loop 的初始化,并在最後建立了 loop 成品 的代理對象

loop 代理對象建立完成之後會将其放入到第一級緩存中(移除第三級緩存中的 loop ,第二級緩存自始至終都沒有 loop )

然後将 loop 代理對象傳回并指派給 半成品 circle 的屬性 loop ,接着進行 半成品 circle 的 initializeBean

因為 circle 的代理對象已經生成過了(在第二級緩存中),是以不用再生成代理對象了;将第二級緩存中的 circle 代理對象移到第一級緩存中,并傳回該代理對象

此時各級緩存中的資料情況如下(普通 circle 、 loop 對象在各自代理對象的 target 中)

Spring 是如何解決循環依賴的?寫得太好了

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

第三級緩存提前建立 circle 代理對象,不提前建立則隻能給 loop 對象的屬性 circle 指派成 半成品 circle ,那麼 loop 對象中的 circle 對象就無 AOP 增強功能了

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

Spring 是如何解決循環依賴的?寫得太好了

第一級緩存存放的是對外暴露的對象,可能是代理對象,也可能是普通對象

是以此種情況下:三級緩存一個都不能少

4、循環依賴 + AOP + 删除第三級緩存

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

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

如果我們打破這個原則,将代理對象的建立邏輯提前,那是不是就可以不用三級緩存了,而隻用兩級緩存了呢?

代碼依舊簡單:spring-circle-custom

https://gitee.com/youzhibing/spring-circle/tree/master/spring-circle-custom

隻是對 Spring 的源碼做了非常小的改動,改動如下:

Spring 是如何解決循環依賴的?寫得太好了

去除了第三級緩存,并将代理對象的建立邏輯提前,置于執行個體化之後,初始化之前;我們來看下執行結果

Spring 是如何解決循環依賴的?寫得太好了

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

5、循環依賴 + AOP + 注解

目前基于 xml 的配置越來越少,而基于注解的配置越來越多,是以了也提供了一個注解的版本供大家去跟源碼

代碼還是很簡單:spring-circle-annotation

https://gitee.com/youzhibing/spring-circle/tree/master/spring-circle-annotation

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

總結

1、三級緩存各自的作用

第一級緩存存的是對外暴露的對象,也就是我們應用需要用到的

第二級緩存的作用是為了處理循環依賴的對象建立問題,裡面存的是半成品對象或半成品對象的代理對象

第三級緩存的作用處理存在 AOP + 循環依賴的對象建立問題,能将代理對象提前建立

2、Spring 為什麼要引入第三級緩存

嚴格來講,第三級緩存并非缺它不可,因為可以提前建立代理對象

提前建立代理對象隻是會節省那麼一丢丢記憶體空間,并不會帶來性能上的提升,但是會破環 Spring 的設計原則

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

是以 Spring 用了第三級緩存,既維持了設計原則,又處理了循環依賴;犧牲那麼一丢丢記憶體空間是願意接受的.