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 中,只有同時滿足以下兩點才能解決循環依賴的問題:
- 依賴的 Bean 必須都是單例。
- 依賴注入的方式,必須不全是構造器注入,且 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 分三步:
- 實例化,createBeanInstance,就是 new 了個對象。
- 屬性注入,populateBean, 就是 set 一些屬性值。
- 初始化,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:
- 一級緩存,singletonObjects,存儲所有已創建完畢的單例 Bean (完整的 Bean)。
- 二級緩存,earlySingletonObjects,存儲所有僅完成實例化,但還未進行屬性注入和初始化的 Bean。
- 三級緩存,singletonFactories,存儲能建立這個 Bean 的一個工廠,通過工廠能獲取這個 Bean,延遲化 Bean 的生成,工廠生成的 Bean 會塞入二級緩存。
這三個 map 是如何配合的呢?
- 首先,獲取單例 Bean 的時候會通過 BeanName 先去 singletonObjects(一級緩存) 查找完整的 Bean,如果找到則直接返回,否則進行步驟 2。
- 看對應的 Bean 是否在創建中,如果不在直接返回找不到,如果是,則會去 earlySingletonObjects (二級緩存)查找 Bean,如果找到則返回,否則進行步驟 3
- 去 singletonFactories (三級緩存)通過 BeanName 查找到對應的工廠,如果存着工廠則通過工廠創建 Bean ,並且放置到 earlySingletonObjects 中。
- 如果三個緩存都沒找到,則返回 null。
從上面的步驟我們可以得知,如果查詢發現 Bean 還未創建,到第二步就直接返回 null,不會繼續查二級和三級緩存。返回 null 之后,說明這個 Bean 還未創建,這個時候會標記這個 Bean 正在創建中,然后再調用 createBean 來創建 Bean,而實際創建是調用方法 doCreateBean。
doCreateBean 這個方法就會執行上面我們說的三步驟:
- 實例化
- 屬性注入
- 初始化
在實例化 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 的生命周期