天天看點

Spring 源碼解析-從源碼角度看bean的循環依賴

作者:網際網路進階架構師

一、概述及目錄

一直想單獨寫一遍關于從源碼角度看bean的循環依賴,因為現在網上的大部分關于循環依賴的文章都是從理論的角度在講述,屬于一些比較硬背的八股文,我以前也看過,背下來了可能面試的時候還好,能夠簡單的說下具體流程,但是如果過了很長時間或者面試問的比較深,就gg了。是以抽這個周末下了Spring源碼,想記錄下源碼角度是怎麼解決Spring 循環依賴的。後續還會有關于Spring AOP相關的章節。

Spring 源碼解析-從源碼角度看bean的循環依賴

二、bean的建立過程

實際上在bean的建立過程中已經包含了循環依賴的相應知識點,話不多少,直接上肝貨哦,部落客趁着周末時間基于源碼簡要整理出來了一個關于bean建立的大緻流程圖。

Spring 源碼解析-從源碼角度看bean的循環依賴

三、什麼是循環依賴,什麼是三級緩存,那又是怎麼執行的?

3.1 什麼是循環依賴

循環依賴:大白話的說就是一個或多個對象執行個體之間存在直接或間接的互相依賴關系,這種依賴關系構成了構成一個環形調用。

  • 第一種情況:自依賴情況
Spring 源碼解析-從源碼角度看bean的循環依賴
  • 第二種情況:兩個對象直接的互相依賴
Spring 源碼解析-從源碼角度看bean的循環依賴
  • 第三種情況:多個對象直接的間接依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

前面兩種情況是直接循環依賴看起來就比較直覺,而第三種屬于間接循環依賴的情況有時候因為業務代碼調用層級很深,很難排查出來。

3.2 循環依賴的主要場景

Spring 源碼解析-從源碼角度看bean的循環依賴

3.3 三級緩存解釋

第一級緩存:單例池 singletonObjects,它用來存放經過完整Bean生命周期過程的單例Bean對象,此時bean的已經完成執行個體化、注⼊、初始化完成。 第二級緩存:earlySingletonObjects,它用來儲存那些沒有經過完整Bean生命周期的單例Bean對象,用來保證不完整的bean也是單例,簡單來說就是剛執行個體化完成的bean,未初始化的 bean 對象。 第三級緩存:singletonFactories,它儲存的就是一個lambda表達式,它主要的作用就是bean出現循環依賴後,某一個bean到底會不會進行AOP操作,也就是我們說的bean工廠,存放ObjectFactory 對象在需要的時候建立代理對象。
Spring 源碼解析-從源碼角度看bean的循環依賴

3.4 代碼次元展示

@Service
public class XiaoMaYi1 {

   @Autowired
   private XiaoMaYi1 xiaoMaYi1;

   public void test1() {
   }
}           
@Service
public class XiaoMaYi2 {

   @Autowired
   private XiaoMaYi1 xiaoMaYi1;

   public void test2() {
   }
}           

拿這個循環依賴,它能正常運⾏,為什麼能夠正常運作,後⾯我們會通過源碼的⻆度,解讀整體的執⾏流程。

下邊還有一個例子:

@Service
public class A {  
    public A(B b) {  }
}           
@Service
public class B {  
    public B(C c) {  
    }
}           
@Service
public class C {  
    public C(A a) {  }
}           

結果啟動發現如下:

Spring 源碼解析-從源碼角度看bean的循環依賴

這就是上邊所說的構造器注入的方式,Spring沒有幫我們解決。

3.5 子產品解讀

Spring 源碼解析-從源碼角度看bean的循環依賴

3.6 相應執行流程

Spring 源碼解析-從源碼角度看bean的循環依賴

源碼流程:

Spring 源碼解析-從源碼角度看bean的循環依賴
大概流程 先去擷取 A 的 Bean,發現沒有就準備去建立⼀個,然後将 A 的代理⼯⼚放⼊“三級緩存”(這個A 其實是⼀個半成品,隻有執行個體化還沒有對⾥⾯的屬性進⾏注⼊),此時發現 A 依賴 B 的建立,就必須先去建立 B; 這時候準備建立 B,發現 B ⼜依賴 A,需要先去建立 A; B中建立A的過,因為在前邊第⼀層已經建立了 A 的代理⼯⼚,直接從“三級緩存”中拿到 A 的代理⼯⼚,擷取 A 的代理對象,放⼊“⼆級緩存”,并清除“三級緩存”; 回到第 2,現在有了 A 的代理對象,對 A 的依賴完美解決(這⾥的 A 仍然是個半成品),B 初始化成功; 回到1,現在 B 初始化成功,完成 A 對象的屬性注⼊,然後再填充 A 的其它屬性,以及 A 的其它步驟(包括 AOP),完成對 A 完整的初始化功能(這⾥的 A 才是完整的 Bean)。 将 A 放⼊“⼀級緩存”。

四:相應源碼解讀

本示例:Spring 的版本是 5.2.x.RELEASE。

4.1 代碼入口解析

Spring 源碼解析-從源碼角度看bean的循環依賴

refresh方法是Spring的核心方法,可以慢慢品下哦!!!

Spring 源碼解析-從源碼角度看bean的循環依賴

這裡過濾掉一些系統的bean,隻關心我們建立的XiaoMaYi相關的bean

Spring 源碼解析-從源碼角度看bean的循環依賴

這裡就可以看到我們建立的xiaomayi的bean了

Spring 源碼解析-從源碼角度看bean的循環依賴

4.2 一級緩存

Spring 源碼解析-從源碼角度看bean的循環依賴

進⼊ doGetBean(),從 getSingleton() 實際上也是預先處理處理好的緩存中 沒有找到對象,進⼊建立 Bean 的邏輯。

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

我們進入createBean方法找到doCreateBean:

Spring 源碼解析-從源碼角度看bean的循環依賴

然後我們就會發現調用了 addSingletonFactory 方法,然後在這裡還會擷取bean的早期引用getEarlyBeanReference

Spring 源碼解析-從源碼角度看bean的循環依賴

将xiaomayi1 工廠對象塞⼊三級緩存 singletonFactories中。

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

進⼊到 populateBean()放置中,然後看執⾏ postProcessProperties(),這⾥是⼀個政策模式,找到下圖的政策對象。

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

下面則都是為擷取 xiaoMaYi1 的成員對象,然後進行屬性注入。

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

進入doResolveDependency這個方法後就是真正找xiaomayi1 依賴的屬性xiaomayi2了。

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

從beanFactory中正式擷取 xiaoMaYi2 的 bean。

Spring 源碼解析-從源碼角度看bean的循環依賴

到這⾥,一級緩存已經結束了,doGetBean 層層嵌套被調用了好多次,有種遞歸算法的感覺. 因為xiaomqyi1依賴 xiaomayi2,下⾯我們進⼊二級緩存處理的邏輯。

4.3 二級緩存

Spring 源碼解析-從源碼角度看bean的循環依賴

圈着的就是二級緩存的依賴邏輯,其實也是先從一級緩存中擷取->建立執行個體->提前暴露,添加到三級緩存->再添加自己依賴的屬性, 這個流程,其實在這裡就是xiaoMaYi2 依賴xiaoMaYi1的流程。

是以前邊和xiaoMaYi1相同的流程我們就直接省略了!!!

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

這裡正式擷取 xiaoMaYi1 的 bean。

到這⾥,二級緩存已經結束了,因為 xiaoMaYi2 依賴 xiaoMaYi1,是以我們進⼊三級緩存看是怎麼處理的。

4.3 三級緩存

Spring 源碼解析-從源碼角度看bean的循環依賴

圖中圈出的為這次說明的流程!!!

擷取 xiaoMaYi1 的 bean,在前邊第⼀層和第⼆層中每次都會從 getSingleton() 擷取依賴的bean,但是由于之前都沒有初始化 xiaoMaYi1 和 xiaoMaYi2 的三級緩存,是以擷取對象都是空對象。那要怎麼處理呢,話不多說,我們開始下邊的流程。

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

這裡是重中之重 來到了第三層,由于第三級緩存中有了 xiaomayi1 的bean,這⾥使⽤三級緩存中的⼯⼚為 xiaomayi1 建立⼀個代理對象,塞⼊ ⼆級緩存。

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

這⾥就拿到了 xiaomayi1 的代理對象,解決了 xiaomayi2 的依賴關系,傳回到二級緩存中。

4.4 傳回二級緩存

傳回傳回二級緩存後,xiaomayi2 初始化結束,這裡有個問題,二級緩存就結束了嗎??? 還有⼆級緩存的資料,啥時候會在⼀級緩存中使用了呢? 心急吃不了熱豆腐,我們繼續。

看這⾥,還記得在 doGetBean() 中,我們會通過 createBean() 建立⼀個 xiaomayi2 的 bean,當 xiaomayi2 的 bean 建立成功後,我們會執⾏ getSingleton(),它會對 xiaomayi2 的結果進⾏處理這個流程嗎???

Spring 源碼解析-從源碼角度看bean的循環依賴

我們進⼊ getSingleton(),會看到下⾯這個⽅法中片段。

Spring 源碼解析-從源碼角度看bean的循環依賴

這個地方就是處理 xiaomayi2 的 ⼀、⼆級緩存的具體邏輯,那麼怎麼處理呢??? 答案就是,将⼆級緩存清除,放⼊⼀級緩存,具體看代碼哦 .

Spring 源碼解析-從源碼角度看bean的循環依賴

4.5 傳回一級緩存

同4.4,xiaomayi1 初始化完畢後,會把 xiaomayi1 的⼆級緩存清除,将對象放⼊⼀級緩存,直接上代碼。

Spring 源碼解析-從源碼角度看bean的循環依賴

到這⾥,所有的流程結束,傳回了我們想要的并且完整的 xiaomayi1 對象。

五:什麼是單例池,什麼是一級緩存?

singletonObjects,結構是 Map,它就是⼀個單例池, 存放已經完全建立好的Bean,那麼什麼叫完完全全建立好的?就是上面說的是,所有的步驟都處理完了,就是建立好的Bean。一個Bean在産的過程中是需要經曆很多的步驟,在這些步驟中可能要處理@Autowired注解,又或是處理@Value ,@Resource, 儲存在該緩存中的Bean所實作Aware子接口的方法已經回調完畢,自定義初始化方法已經執行完畢,也經過BeanPostProcessor實作類的postProcessorBeforeInitialization、postProcessorAfterInitialization方法處理等; 當需要處理的都處理完之後的Bean,就是完全建立好的Bean,這個Bean是可以用來使用的,我們平時在用的Bean其實就是建立好的,同時單例池也是一級緩存的一個别稱。

六:什麼是二級緩存,它的作用是什麼?

二級緩存-earlySingletonObjects 結構就Map<String,ObjectFactory<>>,是用來存放早期暴露的Bean,一般隻有處于循環引用狀态的Bean才會被儲存在該緩存中【早期的意思就是沒有完全建立好,但是由于有循環依賴,就需要把這種Bean提前暴露出去】。儲存在該緩存中的Bean所實作Aware子接口的方法還未回調,自定義初始化方法未執行,也未經過BeanPostProcessor實作類的postProcessorBeforeInitialization、postProcessorAfterInitialization方法處理。如果啟用了Spring AOP,并且處于切點表達式處理範圍之内,那麼會被增強,即建立其代理對象。

七:什麼是三級緩存,它的作用是什麼?

這裡三級緩存就是每個Bean對應的ObjectFactory對象也就是工廠對象,通過調用這個對象的getObject方法,就可以擷取到早期暴露出去的Bean了。

八:為什麼Spring要用三級緩存來解決循環依賴?

這是我們面試過程中常問的一個問題,前⾯了解了bean建立的執⾏流程和源碼解讀,但是沒提到什麼要⽤ 3 級緩存這個問題,這個地方我們解釋下!!!

8.1 . 一級緩存能解決嗎?

Spring 源碼解析-從源碼角度看bean的循環依賴
  • 第一級緩存,也就是緩存完全建立好的Bean的緩存,這個緩存肯定是需要的,因為單例的Bean隻能建立一次,那麼肯定需要第一級緩存存儲這些對象,如果有需要,直接從第一級緩存傳回。那麼如果隻能有二級緩存的話,就隻能舍棄第二級或者第三級緩存。

8.2 . 如果沒有三級緩存?

Spring 源碼解析-從源碼角度看bean的循環依賴
  • 如果沒有三級緩存,也就是沒有ObjectFactory,那麼就需要往第二緩存放入早期的Bean,那麼這個地方就有一個問題,二級緩存應該放什麼東西呢 ? 是放沒有代理的Bean還是被代理的Bean呢?
    • 1: 如果直接往二級緩存添加沒有被代理的Bean,那麼可能注入給其它對象的Bean跟最後最後完全生成的Bean是不一樣的,因為最後生成的是代理對象,這肯定是不允許的;
    • 2: 那麼如果直接往二級緩存添加一個代理Bean呢?
      • 假設沒有循環依賴,提前暴露了代理對象,那麼如果跟最後建立好的不一樣,那麼項目啟動就會報錯,
      • 假設沒有循環依賴,使用了ObjectFactory,那麼就不會提前暴露了代理對象,到最後生成的對象是什麼就是什麼,就不會報錯,
      • 如果有循環依賴,不論怎樣都會提前暴露代理對象,那麼如果跟最後建立好的不一樣,那麼項目啟動就會報錯通過上面分析,如果沒有循環依賴,使用ObjectFactory,就減少了提前暴露代理對象的可能性,進而減少報錯的可能。
      • 那這個對象的代理⼯⼚在這裡有什麼作⽤呢,它的主要作⽤是存放半成品的單例 Bean,⽬的是為了“打破 循環”
      • 還有一個問題 為什麼“三級緩存”不直接存半成品的 XiaoMaYi1,⽽是要存⼀個代理⼯⼚呢 ?這答案就是 AOP了,具體原因我們還是看上邊的源碼來說

從源碼次元我們分析下三級緩存:

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

這裡的重點是主要看這個對象⼯⼚是如何得到的,我們進⼊ getEarlyBeanReference() ⽅法。

Spring 源碼解析-從源碼角度看bean的循環依賴

進入下邊方法

Spring 源碼解析-從源碼角度看bean的循環依賴
Spring 源碼解析-從源碼角度看bean的循環依賴

從這裡就可以看到是處理相應AOP的操作了,也對應了我們上邊說的為什麼要三級緩存的一些點,同時從源碼角度分析:

如果 XiaoMaYi1 有 AOP,就建立⼀個代理對象; 如果 XiaoMaYi1 沒有 AOP,就傳回原對象。

8.3 如果沒有第二級緩存

Spring 源碼解析-從源碼角度看bean的循環依賴

如果沒有第二級緩存, 也就是沒有存放早期的Bean的緩存,其實肯定也不行。上面說過,ObjectFactory其實擷取的對象可能是代理的對象,那麼如果每次都通過ObjectFactory擷取代理對象,那麼每次都重新建立一個代理對象,這肯定也是不允許的。

從上面的分析,我們知道為什麼不能直接使用二級緩存了吧,第三級緩存就是為了避免過早地建立代理對象,進而避免沒有循環依賴過早暴露代理對象産生的問題,而第二級緩存就是防止多次建立代理對象,導緻對象不同。

  • 有了二級緩存都能解決 Spring 依賴了,怎麼要有三級緩存呢。其實我們在前面分析源碼時也提到過,三級緩存主要是解決 Spring AOP 的特性。AOP 本身就是對方法的增強,是 ObjectFactory<?> 類型的 lambda 表達式,而 Spring 的原則又不希望将此類類型的 Bean 前置建立,是以要存放到三級緩存中處理。
  • 其實整體處理過程類似,唯獨是 B 在填充屬性 A 時,先查詢成品緩存、再查半成品緩存,最後在看看有沒有單例工程類在三級緩存中。最終擷取到以後調用 getObject 方法傳回代理引用或者原始引用。

至此也就解決了 Spring AOP 所帶來的三級緩存問題

九:總要有總結

先回顧下本章主要知識點:三級緩存

⼀級緩存:其實就是個單例池,⽤來存放已經初始化完成的單例 Bean; ⼆級緩存:這個主要是為了解決 AOP,存放的是半成品的 AOP 的單例 Bean 三級緩存:這裡主要是解決循環依賴問題,存放的是⽣成半成品單例 Bean 的⼯⼚⽅法。

這是Spring部分循環依賴解決的方法,當然有些方式上邊也說了是處理不了的,比如構造器注入的等。

還有一些其他體會:閱讀源碼剛開始是一個痛苦的過程,但是如果堅持下來自我感覺還是挺有意思,在這個過程中,你可以學習架構的設計模式,同時也了解我們平時用的東西底層是怎麼實作的,像Spring可能我們有時候隻知道項目中用到了,但是底層具體怎麼實作,我們聲明一個bean他是怎麼生成的,我們添加了@Autowired注解為什麼就直接能使用了等等。這些疑問你閱讀debug下源碼都會有答案,而且非常深刻。

作者:小螞蟻技術

連結:https://juejin.cn/post/7212830775970889786

來源:稀土掘金