Spring IOC 容器源碼分析:循環依賴的解決方法

1 簡介

本文,我們來看一下 Spring 是如何解決循環依賴問題的。在本篇文章中,我會首先向大家介紹一下什麼是循環依賴。然後,進入源碼分析階段。爲了更好的說明 Spring 解決循環依賴的辦法,我將會從獲取 bean 的方法getBean(String)開始,把整個調用過程梳理一遍。梳理完後,再來詳細分析源碼。通過這幾步的講解,希望讓大家能夠弄懂什麼是循環依賴,以及如何解循環依賴。

循環依賴相關的源碼本身不是很複雜,不過這裏要先介紹大量的前置知識。不然這些源碼看起來很簡單,但讀起來可能卻也不知所云。那下面我們先來了解一下什麼是循環依賴。

2 背景知識

2.1 什麼是循環依賴

所謂的循環依賴是指,A 依賴 B,B 又依賴 A,它們之間形成了循環依賴。或者是 A 依賴 B,B 依賴 C,C 又依賴 A。它們之間的依賴關係如下:

circular-dependence
這裏以兩個類直接相互依賴爲例,他們的實現代碼可能如下:

public class BeanB {
    private BeanA beanA;
    // 省略 getter/setter
}

public class BeanA {
    private BeanB beanB;
}

配置信息如下:

<bean id="beanA" class="xyz.coolblog.BeanA">
    <property name="beanB" ref="beanB"/>
</bean>
<bean id="beanB" class="xyz.coolblog.BeanB">
    <property name="beanA" ref="beanA"/>
</bean>

IOC 容器在讀到上面的配置時,會按照順序,先去實例化 beanA。然後發現 beanA 依賴於 beanB,接在又去實例化 beanB。實例化 beanB 時,發現 beanB 又依賴於 beanA。如果容器不處理循環依賴的話,容器會無限執行上面的流程,直到內存溢出,程序崩潰。當然,Spring 是不會讓這種情況發生的。在容器再次發現 beanB 依賴於 beanA 時,容器會獲取 beanA 對象的一個早期的引用(early reference),並把這個早期引用注入到 beanB 中,讓 beanB 先完成實例化。beanB 完成實例化,beanA 就可以獲取到 beanB 的引用,beanA 隨之完成實例化。這裏大家可能不知道“早期引用”是什麼意思,這裏先彆着急,我會在下一章進行說明。

好了,本章先到這裏,我們繼續往下看。

2.2 一些緩存的介紹

在進行源碼分析前,我們先來看一組緩存的定義。如下:

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

根據緩存變量上面的註釋,大家應該能大致瞭解他們的用途。我這裏簡單說明一下吧:

緩存 用途
singletonObjects 用於存放完全初始化好的 bean,從該緩存中取出的 bean 可以直接使用
earlySingletonObjects 存放原始的 bean 對象(尚未填充屬性),用於解決循環依賴
singletonFactories 存放 bean 工廠對象,用於解決循環依賴

上一章提到了”早期引用“,所謂的”早期引用“是指向原始對象的引用。所謂的原始對象是指剛創建好的對象,但還未填充屬性。這樣講大家不知道大家聽明白了沒,不過沒聽明白也不要緊。簡單做個實驗就知道了,這裏我們先定義一個對象Room

/** Room 包含了一些電器 */
public class Room {
    private String television;
    private String airConditioner;
    private String refrigerator;
    private String washer;
    // 省略 getter/setter
}

配置如下:

<bean id="room" class="xyz.coolblog.demo.Room">
    <property name="television" value="Xiaomi"/>
    <property name="airConditioner" value="Gree"/>
    <property name="refrigerator" value="Haier"/>
    <property name="washer" value="Siemens"/>
</bean>

我們先看一下完全實例化好後的 bean 長什麼樣的。如下:
room-bean-1
從調試信息中可以看得出,Room的每個成員變量都被賦上值了。然後我們再來看一下“原始的 bean 對象”長的是什麼樣的,如下:
room-bean-2
結果比較明顯了,所有字段都是null。這裏的 bean 和上面的 bean 指向的是同一個對象Room@1567,但現在這個對象所有字段都是null,我們把這種對象成爲原始的對象。形象點說,上面的 bean 對象是一個裝修好的房子,可以拎包入住了。而這裏的 bean 對象還是個毛坯房,還要裝修一下(填充屬性)纔行。

2.3 回顧獲取 bean 的過程

本節,我們來了解從 Spring IOC 容器中獲取 bean 實例的流程(簡化版),這對我們後續的源碼分析會有比較大的幫助。先看圖:
spring-ioc-bean-instance
先來簡單介紹一下這張圖,這張圖是一個簡化後的流程圖。開始流程圖中只有一條執行路徑,在條件sharedInstance != null這裏出現了岔路,形成了綠色和紅色兩條路徑。在上圖中,讀取/添加緩存的方法我用藍色的框和 ☆ 標註了出來。至於虛線的箭頭,和虛線框裏的路徑,這個下面會說到。

我來按照上面的圖,分析一下整個流程的執行順序。這個流程從getBean方法開始,getBean是個空殼方法,所有邏輯都在doGetBean方法中。doGetBean首先會調用getSingleton(beanName)方法獲取sharedInstancesharedInstance可能是完全實例化好的 bean,也可能是一個原始的 bean,當然也有可能是null。如果不爲null,則走綠色的那條路徑。再經getObjectForBeanInstance這一步處理後,綠色的這條執行路徑就結束了。

我們再來看一下紅色的那條執行路徑,也就是sharedInstance = null的情況。在第一次獲取某個 bean 的時候,緩存中是沒有記錄的,所以這個時候要走創建邏輯。上圖中的getSingleton(beanName, new ObjectFactory<Object>() {...})方法會創建一個 bean 實例,上圖虛線路徑指的是getSingleton方法內部調用的兩個方法,其邏輯如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    // 省略部分代碼
    singletonObject = singletonFactory.getObject();
    // ...
    addSingleton(beanName, singletonObject);
}

如上所示,getSingleton會在內部先調用getObject方法創建singletonObject,然後再調用addSingletonsingletonObject放入緩存中。getObject在內部調用了createBean方法,createBean方法基本上也屬於空殼方法,更多的邏輯是寫在doCreateBean方法中的。doCreateBean方法中的邏輯很多,其首先調用了createBeanInstance方法創建了一個原始的 bean 對象,隨後調用addSingletonFactory方法向緩存中添加單例 bean 工廠,從該工廠可以獲取原始對象的引用,也就是所謂的“早期引用”。再之後,繼續調用populateBean方法向原始 bean 對象中填充屬性,並解析依賴。getObject執行完成後,會返回完全實例化好的 bean。緊接着再調用addSingleton把完全實例化好的 bean 對象放入緩存中。到這裏,紅色執行路徑差不多也就要結束的。

我這裏沒有把getObjectaddSingleton方法和getSingleton(String, ObjectFactory)並列畫在紅色的路徑裏,目的是想簡化一下方法的調用棧(都畫進來有點複雜)。我們可以進一步簡化上面的調用流程,比如下面:
get-bean-from-cache
這個流程看起來是不是簡單多了,命中緩存走綠色路徑,未命中走紅色的創建路徑。好了,本節先到這。

3 源碼分析

好了,經過前面的鋪墊,現在我們終於可以深入源碼一探究竟了,想必大家已等不及了。那我不賣關子了,下面我們按照方法的調用順序,依次來看一下循環依賴相關的代碼。如下:

protected <T> T doGetBean(
            final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
            throws BeansException {

    // ...... 
    
    // 從緩存中獲取 bean 實例
    Object sharedInstance = getSingleton(beanName);

    // ......
}

public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 從 singletonObjects 獲取實例,singletonObjects 中的實例都是準備好的 bean 實例,可以直接使用
    Object singletonObject = this.singletonObjects.get(beanName);
    // 判斷 beanName 對應的 bean 是否正在創建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 從 earlySingletonObjects 中獲取提前曝光的 bean
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 獲取相應的 bean 工廠
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 提前曝光 bean 實例(raw bean),用於解決循環依賴
                    singletonObject = singletonFactory.getObject();
                    
                    // 將 singletonObject 放入緩存中,並將 singletonFactory 從緩存中移除
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

上面的源碼中,doGetBean所調用的方法getSingleton(String)是一個空殼方法,其主要邏輯在getSingleton(String, boolean)中。該方法邏輯比較簡單,首先從singletonObjects緩存中獲取 bean 實例。若未命中,再去earlySingletonObjects緩存中獲取原始 bean 實例。如果仍未命中,則從singletonFactory緩存中獲取ObjectFactory對象,然後再調用getObject方法獲取原始 bean 實例的應用,也就是早期引用。獲取成功後,將該實例放入earlySingletonObjects緩存中,並將ObjectFactory對象從singletonFactories移除。看完這個方法,我們再來看看getSingleton(String, ObjectFactory)方法,這個方法也是在doGetBean中被調用的。這次我會把doGetBean的代碼多貼一點出來,如下:

protected <T> T doGetBean(
        final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
        throws BeansException {

    // ...... 
    Object bean;

    // 從緩存中獲取 bean 實例
    Object sharedInstance = getSingleton(beanName);

    // 這裏先忽略 args == null 這個條件
    if (sharedInstance != null && args == null) {
        // 進行後續的處理
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    } else {
        // ......

        // mbd.isSingleton() 用於判斷 bean 是否是單例模式
        if (mbd.isSingleton()) {
            // 再次獲取 bean 實例
            sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
                @Override
                public Object getObject() throws BeansException {
                    try {
                        // 創建 bean 實例,createBean 返回的 bean 是完全實例化好的
                        return createBean(beanName, mbd, args);
                    } catch (BeansException ex) {
                        destroySingleton(beanName);
                        throw ex;
                    }
                }
            });
            // 進行後續的處理
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }

        // ......
    }

    // ......

    // 返回 bean
    return (T) bean;
}

這裏的代碼邏輯和我在「2.3 回顧獲取 bean 的過程」一節的最後貼的主流程圖已經很接近了,對照那張圖和代碼中的註釋,大家應該可以理解doGetBean方法了。繼續往下看:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {

        // ......
        
        // 調用 getObject 方法創建 bean 實例
        singletonObject = singletonFactory.getObject();
        newSingleton = true;

        if (newSingleton) {
            // 添加 bean 到 singletonObjects 緩存中,並從其他集合中將 bean 相關記錄移除
            addSingleton(beanName, singletonObject);
        }

        // ......
        
        // 返回 singletonObject
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 將 <beanName, singletonObject> 映射存入 singletonObjects 中
        this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

        // 從其他緩存中移除 beanName 相關映射
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

上面的代碼中包含兩步操作,第一步操作是調用getObject創建 bean 實例,第二步是調用addSingleton方法將創建好的 bean 放入緩存中。代碼邏輯並不複雜,相信大家都能看懂。那麼接下來我們繼續往下看,這次分析的是doCreateBean中的一些邏輯。如下:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
        throws BeanCreationException {

    BeanWrapper instanceWrapper = null;

    // ......

    // ☆ 創建 bean 對象,並將 bean 對象包裹在 BeanWrapper 對象中返回
    instanceWrapper = createBeanInstance(beanName, mbd, args);
    
    // 從 BeanWrapper 對象中獲取 bean 對象,這裏的 bean 指向的是一個原始的對象
    final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);

    /*
     * earlySingletonExposure 用於表示是否”提前暴露“原始對象的引用,用於解決循環依賴。
     * 對於單例 bean,該變量一般爲 true。更詳細的解釋可以參考我之前的文章
     */ 
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        // ☆ 添加 bean 工廠對象到 singletonFactories 緩存中
        addSingletonFactory(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
                /* 
                 * 獲取原始對象的早期引用,在 getEarlyBeanReference 方法中,會執行 AOP 
                 * 相關邏輯。若 bean 未被 AOP 攔截,getEarlyBeanReference 原樣返回 
                 * bean,所以大家可以把 
                 *      return getEarlyBeanReference(beanName, mbd, bean) 
                 * 等價於:
                 *      return bean;
                 */
                return getEarlyBeanReference(beanName, mbd, bean);
            }
        });
    }

    Object exposedObject = bean;

    // ......
    
    // ☆ 填充屬性,解析依賴
    populateBean(beanName, mbd, instanceWrapper);

    // ......

    // 返回 bean 實例
    return exposedObject;
}

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 將 singletonFactory 添加到 singletonFactories 緩存中
            this.singletonFactories.put(beanName, singletonFactory);

            // 從其他緩存中移除相關記錄,即使沒有
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

上面的代碼簡化了不少,不過看起來仍有點複雜。好在,上面代碼的主線邏輯比較簡單,由三個方法組成。如下:

  1. 創建原始 bean 實例createBeanInstance(beanName, mbd, args)
  2. 添加原始對象工廠對象到singletonFactories緩存中addSingletonFactory(beanName, new ObjectFactory<Object>{...})
  3. 填充屬性,解析依賴populateBean(beanName, mbd, instanceWrapper)

到這裏,本節涉及到的源碼就分析完了。可是看完源碼後,我們似乎仍然不知道這些源碼是如何解決循環依賴問題的。難道本篇文章就到這裏了嗎?答案是否。下面我來解答這個問題,這裏我還是以 BeanA 和 BeanB 兩個類相互依賴爲例。在上面的方法調用中,有幾個關鍵的地方,下面一一列舉出來:

3.1 創建原始 bean 對象

instanceWrapper = createBeanInstance(beanName, mbd, args);
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);

假設 beanA 先被創建,創建後的原始對象爲BeanA@1234,上面代碼中的 bean 變量指向就是這個對象。

3.2 暴露早期引用

addSingletonFactory(beanName, new ObjectFactory<Object>() {
    @Override
    public Object getObject() throws BeansException {
        return getEarlyBeanReference(beanName, mbd, bean);
    }
});

beanA 指向的原始對象創建好後,就開始把指向原始對象的引用通過ObjectFactory暴露出去。getEarlyBeanReference方法的第三個參數 bean 指向的正是createBeanInstance方法創建出原始 bean 對象BeanA@1234

3.3 解析依賴

populateBean(beanName, mbd, instanceWrapper);

populateBean用於向 beanA 這個原始對象中填充屬性,當它檢測到 beanA 依賴於 beanB 時,會首先去實例化 beanB。beanB 在此方法處也會解析自己的依賴,當它檢測到 beanA 這個依賴,於是調用BeanFactry.getBean("beanA")這個方法,從容器中獲取 beanA。

3.4 獲取早期引用

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // ☆ 從緩存中獲取早期引用
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // ☆ 從 SingletonFactory 中獲取早期引用
                    singletonObject = singletonFactory.getObject();
                    
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

接着上面的步驟講,populateBean調用BeanFactry.getBean("beanA")以獲取 beanB 的依賴。getBean("beanA")會先調用getSingleton("beanA"),嘗試從緩存中獲取 beanA。此時由於 beanA 還沒完全實例化好,於是this.singletonObjects.get("beanA")返回null。接着 this.earlySingletonObjects.get("beanA")也返回空,因爲 beanA 早期引用還沒放入到這個緩存中。最後調用singletonFactory.getObject()返回singletonObject,此時singletonObject != nullsingletonObject指向BeanA@1234,也就是createBeanInstance創建的原始對象。此時 beanB 獲取到了這個原始對象的引用,beanB 就能順利完成實例化。beanB 完成實例化後,beanA 就能獲取到 beanB 所指向的實例,beanA 隨之也完成了實例化工作。由於beanB.beanA和 beanA 指向的是同一個對象BeanA@1234,所以 beanB 中的 beanA 此時也處於可用狀態了。

以上的過程對應下面的流程圖:

create-beanA-beanB

4 總結

到這裏,本篇文章差不多就快寫完了,不知道大家看懂了沒。這篇文章在前面做了大量的鋪墊,然後再進行源碼分析。相比於我之前寫的幾篇文章,本篇文章所對應的源碼難度上比之前簡單一些。但說實話也不好寫,我本來只想簡單介紹一下背景知識,然後直接進行源碼分析。但是又怕有的朋友看不懂,所以還是用了大篇幅介紹的背景知識。這樣寫,可能有的朋友覺得比較囉嗦。但是考慮到大家的水平不一,爲了保證讓大家能夠更好的理解,所以還是儘量寫的詳細一點。本篇文章總的來說寫的還是有點累的,花了一些心思思考怎麼安排章節順序,怎麼簡化代碼和畫圖。如果大家看完這篇文章,覺得還不錯的話,不妨給個贊吧,也算是對我的鼓勵吧。

由於個人的技術能力有限,若文章有錯誤不妥之處,歡迎大家指出來。好了,本篇文章到此結束,謝謝大家的閱讀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章