從設計角度,深刻分析 Spring 循環依賴的解決思路

前言

Spring 的循環依賴已經被說爛了,可能不少人也看吐了。但不少博客上說的仍是不夠清楚,沒有完整的表達出 Spring 的設計目的。只介紹了 What ,對於 Why 的介紹卻不太夠。java

本文會從設計角度,一步一步詳細分析 Spring 這個「三級緩存」的設計原則,說說爲何要這麼設計。spring

Bean 建立流程

Spring 中的每個 Bean 都由一個BeanDefinition 建立而來,在註冊完成 BeanDefinition 後。會遍歷BeanFactory中的 beanDefinitionMap 對全部的 Bean 調用 getBean 進行初始化。緩存

簡單來講,一個 Bean 的建立流程主要分爲如下幾個階段:app

  1. Instantiate Bean - 實例化 Bean,經過默認的構造函數或者構造函數注入的方式
  2. Populate Bean - 處理 Bean 的屬性依賴,能夠是Autowired注入的,也能夠是 XML 中配置的,或者是手動建立的 BeanDefinition 中的依賴(propertyValues)
  3. Initialize Bean - 初始化 Bean,執行初始化方法,執行 BeanPostProcessor 。這個階段是各類 Bean 的後置處理,好比 AOP 的代理對象替換就是在這個階段

在完成上面的建立流程後,將 Bean 添加到緩存中 - singletonObjects,之後在 getBean 時先從緩存中查找,不存在才建立。函數

Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

先拋開 Spring 的源碼不談,先看看按照這個建立流程執行會遇到什麼問題post

1. Instantiate Bean

首先是第一階段 - 實例化,就是調用 Bean Class的構造函數,建立實例而已,沒啥可說的。至於一些獲取 BeanDefinition 構造方法的邏輯,不是循環依賴的重點。this

2. Populate Bean

第二階段 - 填充Bean,其目的是查找當前 Bean 引用的全部 Bean,利用 BeanFactory 獲取這些 Bean,而後注入到當前 Bean 的屬性中。spa

正常狀況下,沒有循環引用關係時沒什麼問題。好比如今正在進行 ABean 的 populate 操做,發現了 BBean 的引用,經過 BeanFactory 去 getBean(BBean) 就能夠完成,哪怕如今 BBean 尚未建立,在getBean中完成初始化也能夠。完成後將 BBean 添加到已建立完成的 Bean 緩存中 - singletonObjects。設計

ben_cyclic_ref_0 (1).png

最後再將獲取的 BBean 實例注入到 ABean 中就完成了這個 populate 操做,看着還挺簡單。代理

此時引用關係發生了點變化,ABean 也依賴了 BBean,兩個 Bean 的引用關係變成了互相引用,以下圖所示:ben_cyclic_ref_1.png
再來看看如今 populate 該怎麼執行:

首先仍是先初始化 BBean ,而後發現了 Bbean 引用的 ABean,如今 getBean(ABean),發現 ABean 也沒有建立,開始執行對 ABean 的建立:先實例化,而後對 ABean 執行 populate,可 populate 時又發現了 ABean 引用了 BBean,可此時 BBean 尚未建立完成,Bean 緩存中也並不存在。這樣就出現死循環了,兩個 Bean 相互引用,populate 操做徹底無法執行。

其實解決這個問題也很簡單,出現死循環的關鍵是兩個 Bean 互相引用,populate 時另外一個 Bean 還在建立中,沒有建立完成。

只須要增長一個中間狀態的緩存容器,用來存儲只執行了 instantiate 還未 populate 的那些 Bean。到了populate 階段時,若是完整狀態緩存中不存在,就從中間狀態緩存容器查找一遍,這樣就避免了死循環的問題。

以下圖所示,增長了一箇中間狀態的緩存容器 - earlySingletonObjects,用來存儲剛執行 instantiate 的 Bean,在 Bean 完成建立後,從 earlySingletonObjects 刪除,添加到 singletonObjects 中。
ben_cyclic_ref_1 (1).png
回到上面的例子,若是在 ABean 的 populate 階段又發現了 BBean 的引用,那麼先從 singletonObjects 查找,若是不存在,繼續從 earlySingletonObjects 中查找,找到之後注入到 ABean 中,而後 ABean 建立完成(BeanPostProcessor待會再說)。如今將 ABean 添加到 singletonObjects 中,接着返回到建立 BBean 的過程。最後把返回的 ABean 注入到 BBean 中,就完成了 BBean 的 populate 操做,以下圖所示:

ben_cyclic_ref_7 (1).png

循環依賴的問題,就這麼輕易的解決了,看着很簡單,只是加了一箇中間狀態而已。但除了 instantiate 和 populate 階段,還有最後一個執行 BeanPostProcessor 階段,這個階段可能會加強/替換原始 Bean

3. Initialize Bean

這個階段分爲執行初始化方法 - initMethod,和執行 BeanFactory 中定義的 BeanPostProcessor(BPP)。執行初始化方法沒啥可說的,重點看看 執行BeanPostProcessor 部分。

BeanPostProcessor 算是 Spring 的靈魂接口了,不少擴展的操做和功能都是經過這個接口,好比 AOP。在populate 完成以後,Spring 會對 Bean 順序執行全部的 BeanPostProcessor,而後返回 BeanPostProcessor 返回的新 Bean 實例(可能有修改也可能沒修改)

//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
    throws BeansException {

    Object result = existingBean;
    //對當前 Bean 順序的執行全部的 BeanPostProcessor,並返回
    for (BeanPostProcessor processor : getBeanPostProcessors()) {
        Object current = processor.postProcessBeforeInitialization(result, beanName);
        if (current == null) {
            return result;
        }
        result = current;
    }
    return result;
}

Spring 的 AOP 加強功能,也是利用 BeanPostProcessor 完成的,若是該 Bean 有 AOP 加強的配置,那麼執行完 BeanPostProcessor 以後就會返回一個新的 Bean,最後存儲到 singletonObjects 中的也是這個加強以後的 Bean

可咱們上面的「中間狀態緩存」解決方案中,存儲的卻只是剛執行完成 instantiate 的 Bean。若是在上面循環依賴的例子中,populate ABean 時因爲 BBean 只完成了實例化,因此會從 earlySingletonObjects 獲取只完成初始化的 BBean 並注入到 ABean中。

若是 BBean 有 AOP 的配置,那麼此時注入到 ABean 中的 只是一個只實例化未 AOP 加強的對象。當 BBean 執行 BeanPostProcessor 後,又會建立一個加強的 BBean 實例,最終添加到 singletonObjects 中的,是加強的 BBean 實例,而不是那個剛實例化的 BBean 實例

以下圖所示,BBean 中注入的是黃色的只完成了初始化的 ABbean,而最終添加到 singletonObjects 倒是執行完 AOP 的加強 ABean 實例:
ben_cyclic_ref_2 (1).png
因爲 populate 以後還有一步 BeanPostProcessor 的加強,致使咱們上面的解決方案無效了。但也不是徹底無解,若是可讓加強型的 BeanPostProcessor 提早執行,而後添加到「中間狀態的緩存容器」中,是否是也能夠解決問題?

不過並非全部的 Bean 都有 AOP(及其餘執行 BPP 後返回新對象) 的需求,若是讓全部 Bean 都提早執行 BeanPostProcessor 並不合適。

因此這裏能夠採用一種「延遲處理」的方式,在中間增長一層 Factory,在這個 Factory 中完成「提早執行」的操做。

若是沒有提早調用某個 Bean 的 「延遲處理」Factory,那麼就不會致使提早執行 BeanPostProcessor,只有循環依賴場景下,纔會出現這種只完成初始化卻未徹底建立的 Bean ,纔會調用這個 Factory。這個 Factory 的模式就叫延遲處理,若是不調用 Factory 就不會提早執行 BPP。

instantiate 完成後,把這個 Factory 添加到「中間狀態的緩存容器」中;這樣當發生循環依賴時,原先獲取的中間狀態 Bean 實例就會變成這個 Factory,此時執行這個Factory 就能夠完成「提早執行 BeanPostProcessor」的操做,而且獲取執行後的新 Bean 實例

如今增長一個 ObjectFactory,用來實現延遲處理:

public interface ObjectFactory<T> {

    T getObject() throws BeansException;

}

而後再建立一個 singletonFactories,做爲咱們新的中間狀態緩存容器,不過這個容器存儲的並非 Bean 實例,而是建立 Bean 的實現代碼

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

如今再寫一個「提早執行」BeanPostProcessor 的 ObjectFactory,添加到 singletonFactories 中。

//完成bean 的 instantiate 後
//建立一個對該 Bean 提早執行 BeanPostProcessor 的 ObjectFactory
//最後添加到 singletonFactories 中
addSingletonFactory(beanName, 
                    () -> getEarlyBeanReference(beanName, mbd, bean)
                   );

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            this.singletonFactories.put(beanName, singletonFactory);
            ......
        }
    }
}

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

再次回到上面循環依賴的例子,若是在對 ABean 執行 populate 時,又發現了 BBean 的引用,那麼此時先從咱們這個新的延遲處理+提早執行的緩存容器中查找,不過如今找到的已經再也不是一個 BBean 實例了,而是咱們上面定義的那個getEarlyBeanReference 的 ObjectFactory ,經過調用 ObjectFactory.getObject() 來獲取提早執行 BeanPostProcessor 的這個 ABean 實例。

以下圖所示,在對 ABean 執行 populate 時,發現了對 BBean 的引用,那麼直接從 singletonFactories 中查找 BBean 的 ObjectFactory 並執行,獲取 BeanPostProcessor 加強/替換後的新 Bean

ben_cyclic_ref_3 (1).png

如今因爲咱們的中間狀態數據從 Bean 實例變成了 ObjectFactory,因此還須要在初始化以後,再檢查一下 singletonFactories 是否有當前 Bean,若是有的化須要手動調用一下 getObject 來獲取最終的 Bean 實例。

經過「延遲執行」+「提早執行」兩個操做,終於解決了這個循環依賴的問題。不過提早執行 BeanPostProcessor 會致使最終執行兩遍 BeanPostProcessor ,這個執行兩遍的問題還須要處理。

這個問題解決倒還算簡單,在那些會更換原對象實例的 BeanPostProcessor 中增長一個緩存,用來存儲已經加強的 Bean ,每次調用該 BeanPostProcessor 的時候,若是緩存中已經存在那就說明建立過了,直接返回上次建立的便可。Spring 爲此還單獨設計了一個接口,命名也很形象 - SmartInstantiationAwareBeanPostProcessor

若是你定義的 BeanPostProcessor 會加強並替換原有的 Bean 實例,必定要實現這個接口,在實現內進行緩存,避免重複加強

貌似如今問題已經解決了,一開始設計的 earlySingletonObjects 也不須要了,直接使用咱們這個中間狀態緩存工廠 - singletonFactories 就搞定了問題。

不過……若是依賴關係再複雜一點,好比像下面這樣,ABean 中有兩個屬性都引用了 BBean
ben_cyclic_ref_0 (2).png
那麼在對 ABean 執行 populate 時,先處理 refB 這個屬性;此時從 singletonFactories 中查找到 BBean 的這個提早執行 BeanPostProcessor 的 ObjectFactory,調用 getObject 獲取到提早執行 BeanPostProcessor 的 BBean 實例,注入到 refB 屬性中。

那到了 refB1 這個屬性時,因爲 BBean 仍是一個沒有建立完成的狀態(singletonObjects 中不存在),因此仍然須要獲取 BBean 的 ObjectFactory,執行 getObject,致使又對 BBean 執行了一遍 BeanPostProcessor。

爲了處理這個屢次引用的問題,仍是須要有一箇中間狀態的緩存容器 - earlySingletonObjects。不過這個緩存容器和一開始提到的那個 earlySingletonObjects 有一點點不一樣;一開始提到的 earlySingletonObjects 是存儲只執行了 instantiate 狀態的 Bean 實例,而咱們如今存儲的是執行 instantiate 以後,又提早執行了 BeanPostProcessor 的那些 Bean。

在提早執行了 BeanPostProcessor 以後,將返回的新的 Bean 實例也添加到 earlySingletonObjects 這個緩存容器中。這樣就算處於中間狀態時有屢次引用(屢次 getBean),也能夠從 earlySingletonObjects 獲取已經執行完 BeanPostProcessor 的那個 Bean,不會形成重複執行的問題。

總結

回顧一下上面一步步解決循環依賴的流程,最終咱們經過一個延遲處理的緩存容器,加一個提早執行完畢BeanPostProcessor 的中間狀態容器就完美解決了循環依賴的問題

至於 singletonObjects 這個緩存容器,它只用來存儲全部建立完成的 Bean,和處理循環依賴關係並不大。

至於這個處理機制,叫不叫「三級緩存」……見仁見智吧,Spring 在源碼/註釋中也沒有(3-level cache之類的字眼)。並且關鍵的循環依賴處理,只是「二級」(延遲處理的 Factory + 提早執行 BeanPostProcessor 的Bean),所謂的「第三級」是應該是指 singletonObjects。

下面用一張圖,簡單總結一下處理循環依賴的核心機制:
ben_cyclic_ref_5.png
不過提早執行 BeanPostProcessor 這個操做,算不算打破了原有的設計呢?本來 BeanPostProcessor 但是在建立Bean 的最後階段執行的,可如今爲了處理循環依賴,給移動到 populate 以前了。雖然是一個不太優雅的設計,但用來解決循環依賴也不錯。

儘管 Spring 支持了循環依賴(僅支持屬性依賴方式,構造方法依賴不支持,由於實例化都完成不了),但實際項目中,這種循環依賴的關係每每是不合理的,應該從設計上就避免。

原創不易,禁止未受權的轉載。若是個人文章對您有幫助,就請點贊/收藏/關注鼓勵支持一下吧❤❤❤❤❤❤
相關文章
相關標籤/搜索