Spring 如何解決循環依賴


Spring 如何解決循環依賴

 

 

1、簡介

1.1、什么是循環依賴

例如下方代碼:

@Service public class A { @Autowired private B b; } @Service public class B { @Autowired private A a; } //或者自己依賴自己
@Service public class A { @Autowired private A a; }

上面這兩種方式都是循環依賴,應該很好理解,當然也可以是三個 Bean 甚至更多的 Bean 相互依賴,原理都是一樣的,今天我們主要分析兩個 Bean 的依賴。

這種循環依賴可能會產生問題,例如 A 要依賴 B,發現 B 還沒創建。於是開始創建 B ,創建的過程發現 B 要依賴 A, 而 A 還沒創建好呀,因為它要等 B 創建好。就這樣它們倆就擱這卡 bug 了。

 

2、Spring 如何解決循環依賴

上面這種循環依賴在實際場景中是會出現的,所以 Spring 需要解決這個問題,那如何解決呢?

關鍵就是提前暴露未完全創建完畢的 Bean。

在 Spring 中,只有同時滿足以下兩點才能解決循環依賴的問題:

  1. 依賴的 Bean 必須都是單例。
  2. 依賴注入的方式,必須不全是構造器注入,且 beanName 字母序在前的不能是構造器注入。

2.1、為什么必須都是單例?

如果從源碼來看的話,循環依賴的 Bean 是原型模式,會直接拋錯:

所以 Spring 只支持單例的循環依賴,但是為什么呢?

        按照理解,如果兩個 Bean 都是原型模式的話。那么創建 A1 需要創建一個 B1。創建 B1 的時候要創建一個 A2。創建 A2 又要創建一個 B2。創建 B2 又要創建一個 A3。創建 A3 又要創建一個 B3.....

就又卡 BUG 了,是吧,因為原型模式都需要創建新的對象,不能跟用以前的對象。如果是單例的話,創建 A 需要創建 B,而創建的 B 需要的是之前的個 A, 不然就不叫單例了,對吧?

具體做法就是:先創建 A,此時的 A 是不完整的(沒有注入 B),用個 map 保存這個不完整的 A,再創建 B ,B 需要 A。

所以從那個 map 得到“不完整”的 A,此時的 B 就完整了,然后 A 就可以注入 B,然后 A 就完整了,B 也完整了,且它們是相互依賴的。

2.2、為什么不能全是構造器注入?

在 Spring 中創建 Bean 分三步:

  1. 實例化,createBeanInstance,就是 new 了個對象。
  2. 屬性注入,populateBean, 就是 set 一些屬性值。
  3. 初始化,initializeBean,執行一些 aware 接口中的方法,initMethod,AOP代理等。

明確了上面這三點,再結合我上面說的“不完整的”,我們來理一下。

如果全是構造器注入,比如A(B b),那表明在 new 的時候,就需要得到 B,此時需要 new B 。

但是 B 也是要在構造的時候注入 A ,即B(A a),這時候 B 需要在一個 map 中找到不完整的 A ,發現找不到。

為什么找不到?因為 A 還沒 new 完呢,所以找到不完整的 A,因此如果全是構造器注入的話,那么 Spring 無法處理循環依賴。

 

2.3、一個set注入,一個構造器注入一定能成功?

1、假設我們 A 是通過 set 注入 B,B 通過構造函數注入 A,此時是成功的。

我們來分析下:實例化 A 之后,可以在 map 中存入 A,開始為 A 進行屬性注入,發現需要 B。此時 new B,發現構造器需要 A,此時從 map 中得到 A ,B 構造完畢。B 進行屬性注入,初始化,然后 A 注入 B 完成屬性注入,然后初始化 A。整個過程很順利,沒毛病。

2、假設 A 是通過構造器注入 B,B 通過 set 注入 A,此時是失敗的。

我們來分析下:實例化 A,發現構造函數需要 B, 此時去實例化 B。然后進行 B 的屬性注入,從 map 里面找不到 A,因為 A 還沒 new 成功,所以 B 也卡住了,然后就 gg。

看到這里,仔細思考的小伙伴可能會說,可以先實例化 B 啊,往 map 里面塞入不完整的 B,這樣就能成功實例化 A 了啊。

確實,思路沒錯但是 Spring 容器是按照字母序創建 Bean 的,A 的創建永遠排在 B 前面。

現在我們總結一下:

  • 如果循環依賴都是構造器注入,則失敗
  • 如果循環依賴不完全是構造器注入,則可能成功,可能失敗,具體跟BeanName的字母序有關系。

 

3、Spring 解決循環依賴全流程

經過上面的鋪墊,我想你對 Spring 如何解決循環依賴應該已經有點感覺了,接下來我們就來看看它到底是如何實現的。

明確了 Spring 創建 Bean 的三步驟之后,我們再來看看它為單例搞的三個 map:

  1. 一級緩存singletonObjects,存儲所有已創建完畢的單例 Bean (完整的 Bean)。
  2. 二級緩存earlySingletonObjects,存儲所有僅完成實例化,但還未進行屬性注入和初始化的 Bean。
  3. 三級緩存singletonFactories,存儲能建立這個 Bean 的一個工廠,通過工廠能獲取這個 Bean,延遲化 Bean 的生成,工廠生成的 Bean 會塞入二級緩存。

這三個 map 是如何配合的呢?

  1. 首先,獲取單例 Bean 的時候會通過 BeanName 先去 singletonObjects(一級緩存) 查找完整的 Bean,如果找到則直接返回,否則進行步驟 2。
  2. 看對應的 Bean 是否在創建中,如果不在直接返回找不到,如果是,則會去 earlySingletonObjects (二級緩存)查找 Bean,如果找到則返回,否則進行步驟 3
  3. 去 singletonFactories (三級緩存)通過 BeanName 查找到對應的工廠,如果存着工廠則通過工廠創建 Bean ,並且放置到 earlySingletonObjects 中。
  4. 如果三個緩存都沒找到,則返回 null。

       從上面的步驟我們可以得知,如果查詢發現 Bean 還未創建,到第二步就直接返回 null,不會繼續查二級和三級緩存。返回 null 之后,說明這個 Bean 還未創建,這個時候會標記這個 Bean 正在創建中,然后再調用 createBean 來創建 Bean,而實際創建是調用方法 doCreateBean。

doCreateBean 這個方法就會執行上面我們說的三步驟:

  1. 實例化
  2. 屬性注入
  3. 初始化

在實例化 Bean 之后,會往 singletonFactories 塞入一個工廠,而調用這個工廠的 getObject 方法,就能得到這個 Bean。

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

       要注意,此時 Spring 是不知道會不會有循環依賴發生的,但是它不管,反正往 singletonFactories 塞這個工廠,這里就是提前暴露。然后就開始執行屬性注入,這個時候 A 發現需要注入 B,所以去 getBean(B),此時又會走一遍上面描述的邏輯,到了 B 的屬性注入這一步。此時 B 調用 getBean(A),這時候一級緩存里面找不到,但是發現 A 正在創建中的,於是去二級緩存找,發現沒找到,於是去三級緩存找,然后找到了。並且通過上面提前在三級緩存里暴露的工廠得到 A,然后將這個工廠從三級緩存里刪除,並將 A 加入到二級緩存中。然后結果就是 B 屬性注入成功。緊接着 B 調用 initializeBean 初始化,最終返回,此時 B 已經被加到了一級緩存里 。這時候就回到了 A 的屬性注入,此時注入了 B,接着執行初始化,最后 A 也會被加到一級緩存里,且從二級緩存中刪除 A。

Spring 解決依賴循環就是按照上面所述的邏輯來實現的。

重點就是在對象實例化之后,都會在三級緩存里加入一個工廠,提前對外暴露還未完整的 Bean,這樣如果被循環依賴了,對方就可以利用這個工廠得到一個不完整的 Bean,破壞了循環的條件。

 

4、為什么循環依賴需要三級緩存,二級不夠嗎?

       很明顯,如果僅僅只是為了破解循環依賴,二級緩存夠了,壓根就不必要三級。你思考一下,在實例化 Bean A 之后,我在二級 map 里面塞入這個 A,然后繼續屬性注入。發現 A 依賴 B 所以要創建 Bean B,這時候 B 就能從二級 map 得到 A ,完成 B 的建立之后, A 自然而然能完成。所以為什么要搞個三級緩存,且里面存的是創建 Bean 的工廠呢?

我們來看下調用工廠的 getObject 到底會做什么,實際會調用下面這個方法:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject = bp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject; }

        重點就在中間的判斷,如果 false,返回就是參數傳進來的 bean,沒任何變化。如果是 true 說明有 InstantiationAwareBeanPostProcessors 。且循環的是 smartInstantiationAware 類型,如有這個 BeanPostProcessor 說明 Bean 需要被 aop 代理。我們都知道如果有代理的話,那么我們想要直接拿到的是代理對象。也就是說如果 A 需要被代理,那么 B 依賴的 A 是已經被代理的 A,所以我們不能返回 A 給 B,而是返回代理的 A 給 B。這個工廠的作用就是判斷這個對象是否需要代理,如果否則直接返回,如果是則返回代理對象。看到這明白的小伙伴肯定會問,那跟三級緩存有什么關系,我可以在要放到二級緩存的時候判斷這個 Bean 是否需要代理,如果要直接放代理的對象不就完事兒了。是的,這個思路看起來沒任何問題,問題就出在時機,這跟 Bean 的生命周期有關系。正常代理對象的生成是基於后置處理器,是在被代理的對象初始化后期調用生成的,所以如果你提早代理了其實是違背了 Bean 定義的生命周期。所以 Spring 先在一個三級緩存放置一個工廠,如果產生循環依賴,那么就調用這個工廠提早得到代理對象。如果沒產生依賴,這個工廠根本不會被調用,所以 Bean 的生命周期就是對的。

至此,我想你應該明白為什么會有三級緩存了。

也明白,其實破壞循環依賴,其實只有二級緩存就夠了,但是礙於生命周期的問題,提前暴露工廠延遲代理對象的生成。

對了,不用擔心三級緩存因為沒有循環依賴,數據堆積的問題,最終單例 Bean 創建完畢都會加入一級緩存,此時會清理下面的二、三級緩存。

 

5、總結:

  • 有構造器注入,不一定會產生問題,具體得看是否都是構造器注和 BeanName 的字母序
  • 如果單純為了打破循環依賴,不需要三級緩存,兩級就夠了。
  • 三級緩存是否為延遲代理的創建,盡量不打破 Bean 的生命周期

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM