天天看點

Spring 解決循環依賴過程

有構造器就無法解決循環依賴?

一定要三級緩存才能解決循環依賴?

到底為什麼要三級緩存?

  • 有構造器就無法解決循環依賴?
  • 一定要三級緩存才能解決循環依賴?
  • 到底為什麼要三級緩存?
@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 的依賴。

Spring 解決循環依賴過程

這種循環依賴可能會産生問題,例如 A 要依賴 B,發現 B 還沒建立。

于是開始建立 B ,建立的過程發現 B 要依賴 A, 而 A 還沒建立好呀,因為它要等 B 建立好。

就這樣 它們倆就擱這卡 bug 了 。

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

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

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

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

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

Spring 解決循環依賴過程

是以 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 解決循環依賴過程

讀起來好像有點繞,但是邏輯其實很清晰。

在 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 無法處理循環依賴 。假設我們 A 是通過 set 注入 B,B 通過構造函數注入 A,此時是 成功的 。

我們來分析下:

執行個體化 A 之後,可以在 map 中存入 A,開始為 A 進行屬性注入,發現需要 B。此時 new B,發現構造器需要 A,此時從 map 中得到 A ,B 構造完畢。

B 進行屬性注入,初始化,然後 A 注入 B 完成屬性注入,然後初始化 A。整個過程很順利,沒毛病。

Spring 解決循環依賴過程

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

我們來分析下:執行個體化 A,發現構造函數需要 B, 此時去執行個體化 B。

然後進行 B 的屬性注入,從 map 裡面找不到 A,因為 A 還沒 new 成功,是以 B 也卡住了,然後就 gg。

Spring 解決循環依賴過程

看到這裡,仔細思考的小夥伴可能會說,可以先執行個體化 B 啊,往 map 裡面塞入不完整的 B,這樣就能成功執行個體化 A 了啊。

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

現在我們總結一下:

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

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

明确了 Spring 建立 Bean 的三步驟之後,我們再來看看它為單例搞的​

​三個 map:​

  1. 一級緩存,singletonObjects,存儲所有已建立完畢的單例 Bean , 擁有完整生命周期的 bean 對象(完整的 Bean)
  2. 二級緩存,earlySingletonObjects,存儲所有僅完成執行個體化,但還未進行屬性注入和初始化的 Bean, 如果提前進行了 AOP,存放的就是半成品代理對象
  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,破壞了循環的條件。

上面都說了那麼多了,那我們思考下,解決循環依賴需要三級緩存嗎?

很明顯,如果僅僅隻是為了破解循環依賴,二級緩存夠了,壓根就不必要三級。

你思考一下,在執行個體化 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 解決循環依賴過程

好了,看到這裡想必你應該對 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 再完成它的整個生命周期。至此,循環依賴結束!

面試官:” 為什麼要使用三級緩存呢?二級緩存能解決循環依賴嗎?“