有構造器就無法解決循環依賴?
一定要三級緩存才能解決循環依賴?
到底為什麼要三級緩存?
- 有構造器就無法解決循環依賴?
- 一定要三級緩存才能解決循環依賴?
- 到底為什麼要三級緩存?
@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 了 。
上面這種循環依賴在實際場景中是會出現的,是以 Spring 需要解決這個問題,那如何解決呢?
關鍵就是 提前暴露未完全建立完畢的 Bean 。
在 Spring 中,隻有同時滿足以下兩點才能解決循環依賴的問題:
- 依賴的 Bean 必須都是單例
- 依賴注入的方式,必須 不全是 構造器注入,且 beanName 字母序在前的不能是構造器注入
如果從源碼來看的話,循環依賴的 Bean 是原型模式,會直接抛錯:
是以 Spring 隻支援單例的循環依賴, 但是為什麼呢 ?
按照了解,如果兩個 Bean 都是原型模式的話。
那麼建立 A1 需要建立一個 B1。
建立 B1 的時候要建立一個 A2。
建立 A2 又要建立一個 B2。
建立 B2 又要建立一個 A3。
建立 A3 又要建立一個 B3…
就又卡 BUG 了,是吧,因為原型模式都需要建立新的對象,不能跟用以前的對象。
如果是單例的話,建立 A 需要建立 B,而建立的 B 需要的是之前的個 A, 不然就不叫單例了,對吧?
也是基于這點, Spring 就能操作操作了。
具體做法就是:先建立 A,此時的 A 是不完整的(沒有注入 B),用個 map 儲存這個不完整的 A,再建立 B ,B 需要 A。
是以從那個 map 得到 “不完整” 的 A,此時的 B 就完整了,然後 A 就可以注入 B,然後 A 就完整了,B 也完整了,且它們是互相依賴的。
讀起來好像有點繞,但是邏輯其實很清晰。
在 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 無法處理循環依賴 。假設我們 A 是通過 set 注入 B,B 通過構造函數注入 A,此時是 成功的 。
我們來分析下:
執行個體化 A 之後,可以在 map 中存入 A,開始為 A 進行屬性注入,發現需要 B。此時 new B,發現構造器需要 A,此時從 map 中得到 A ,B 構造完畢。
B 進行屬性注入,初始化,然後 A 注入 B 完成屬性注入,然後初始化 A。整個過程很順利,沒毛病。
假設 A 是通過構造器注入 B,B 通過 set 注入 A,此時是 失敗的 。
我們來分析下:執行個體化 A,發現構造函數需要 B, 此時去執行個體化 B。
然後進行 B 的屬性注入,從 map 裡面找不到 A,因為 A 還沒 new 成功,是以 B 也卡住了,然後就 gg。
看到這裡,仔細思考的小夥伴可能會說,可以先執行個體化 B 啊,往 map 裡面塞入不完整的 B,這樣就能成功執行個體化 A 了啊。
确實,思路沒錯 但是 Spring 容器是按照字母序建立 Bean 的,A 的建立永遠排在 B 前面 。
現在我們總結一下:
- 如果循環依賴都是構造器注入,則失敗
- 如果循環依賴不完全是構造器注入,則可能成功,可能失敗,具體跟 BeanName 的字母序有關系。
經過上面的鋪墊,我想你對 Spring 如何解決循環依賴應該已經有點感覺了,接下來我們就來看看它到底是如何實作的。
明确了 Spring 建立 Bean 的三步驟之後,我們再來看看它為單例搞的
三個 map:
- 一級緩存,singletonObjects,存儲所有已建立完畢的單例 Bean , 擁有完整生命周期的 bean 對象(完整的 Bean)
- 二級緩存,earlySingletonObjects,存儲所有僅完成執行個體化,但還未進行屬性注入和初始化的 Bean, 如果提前進行了 AOP,存放的就是半成品代理對象
- 三級緩存,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,破壞了循環的條件。
上面都說了那麼多了,那我們思考下,解決循環依賴需要三級緩存嗎?
很明顯,如果僅僅隻是為了破解循環依賴,二級緩存夠了,壓根就不必要三級。
你思考一下,在執行個體化 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 建立完畢都會加入一級緩存,此時會清理下面的二、三級緩存。
好了,看到這裡想必你應該對 Spring 的循環依賴很清晰了,并且面試的時候肯定也難不倒你了。
我稍微總結下:
- 有構造器注入,不一定會産生問題,具體得看是否都是構造器注和 BeanName 的字母序
- 如果單純為了打破循環依賴,不需要三級緩存,兩級就夠了。
- 三級緩存是否為延遲代理的建立,盡量不打破 Bean 的生命周期
總結
面試官:”Spring 是如何解決的循環依賴?“
答:Spring 通過三級緩存解決了循環依賴,其中一級緩存為單例池(singletonObjects), 二級緩存為早期曝光對象 earlySingletonObjects,三級緩存為早期曝光對象工廠(singletonFactories)。當 A、B 兩個類發生循環引用時,在 A 完成執行個體化後,就使用執行個體化後的對象去建立一個對象工廠,并添加到三級緩存中,如果 A 被 AOP 代理,那麼通過這個工廠擷取到的就是 A 代理後的對象,如果 A 沒有被 AOP 代理,那麼這個工廠擷取到的就是 A 執行個體化的對象。當 A 進行屬性注入時,會去建立 B,同時 B 又依賴了 A,是以建立 B 的同時又會去調用 getBean(a) 來擷取需要的依賴,此時的 getBean(a) 會從緩存中擷取,第一步,先擷取到三級緩存中的工廠;第二步,調用對象工廠的 getObject 方法來擷取到對應的對象,得到這個對象後将其注入到 B 中。緊接着 B 會走完它的生命周期流程,包括初始化、後置處理器等。當 B 建立完後,會将 B 再注入到 A 中,此時 A 再完成它的整個生命周期。至此,循環依賴結束!
面試官:” 為什麼要使用三級緩存呢?二級緩存能解決循環依賴嗎?“