spring原始碼閱讀筆記09:迴圈依賴

木瓜芒果發表於2020-04-27

  前面的文章一直在研究Spring建立Bean的整個過程,建立一個bean是一個非常複雜的過程,而其中最難以理解的就是對迴圈依賴的處理,本文就來研究一下spring是如何處理迴圈依賴的。

 

1. 什麼是迴圈依賴

  不管之前是否研究過迴圈依賴,這裡先對這個知識做一點回顧。

  迴圈依賴就是迴圈引用,就是兩個或者多個bean相互之間的持有對方,比如A引用B,B引用C,C引用A,則它們最終反映為一個環,參考下圖:

 

  瞭解了什麼是迴圈依賴之後,我們知道這是一種不可避免會出現的情況,那作為Bean容器的Spring又是怎麼處理這一問題呢?我們接著往下看。

 

2. Spring如何處理迴圈依賴

  Spring容器迴圈依賴包括構造器迴圈依賴和setter迴圈依賴,那Spring容器又是如何解決迴圈依賴的呢?我們來測試一下,首先我們來定義迴圈引用類:

public class TestA{
    private TestB testB;

    public void a(){
        testB.b();
    }

    public TestB getTestB(){
        return testB;
    }

    public void setTestB(TestB testB){
        this.testB = testB;
    }
}

public class TestB{
    private TestC testC;

    public void b(){
        testC.c();
    }

    public TestC getTestC(){
        return testC;
    }

    public void setTestC(TestC testC){
        this.testC = testC;
    }
}

public class TestC{
    private TestA testA;

    public void c(){
        testA.a();
    }

    public TestA getTestA(){
        return testA;
    }

    public void setTestA(TestA testA){
        this.testA = testA;
    }
}

  在Spring中將迴圈依賴的處理分成了3種情況:

2.1 構造器迴圈依賴處理

  這表示通過構造器注入構成的迴圈依賴,此依賴是無法解決的,只能丟擲BeanCurrentlyInCreationException異常表示迴圈依賴。

  比如在建立TestA類時,構造器需要TestB類,那麼將去建立TestB,在建立TestB類時又發現需要TestC類,則又去建立TestC,最終在建立TestC時發現又需要TestA,從而形成一個環,沒辦法建立。

  Spring容器將每一個正在建立的bean識別符號放在一個“當前建立bean池”中,bean識別符號在建立過程中將一直保持在這個池中,因此如果在建立bean的過程中發現自己已經在“當前建立bean池”裡時,則丟擲BeanCurrentlyInCreationException異常表示出現了迴圈依賴;而對於建立完畢的bean將從“當前建立bean池”中清除掉,這個“當前建立bean池”實際上是一個ConcurrentHashMap,即DefaultSingletonBeanRegistry中的singletonsCurrentlyInCreation。

  我們通過一個直觀的測試用例來進行分析:

  xml配置如下:

    <bean id = "testA" class = "xxx.xxx">
        <constructor-arg index = "0" ref = "testB"/>
    </bean>
    <bean id = "testB" class = "xxx.xxx">
        <constructor-arg index = "0" ref = "testC"/>
    </bean>
    <bean id = "testC" class = "xxx.xxx">
        <constructor-arg index = "0" ref = "testA"/>
    </bean>

  建立測試用例:

public static void main(String[] args) {
    try{
        new ClassPathXmlApplicationContext("beans.xml");
    }catch (Exception e){
        e.printStackTrace();
    }
}

  這個執行過程中會丟擲異常BeanCurrentlyInCreationException,通過debug可以快速找到異常丟擲的位置在getSingleton()方法中的beforeSingletonCreation():

protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.containsKey(beanName) &&
            this.singletonsCurrentlyInCreation.put(beanName, Boolean.TRUE) != null) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

  由此可知,Spring在對構造器迴圈依賴的處理策略上是選擇了直接拋異常,而且對迴圈依賴的判斷是發生在載入單例時呼叫ObjectFactory的getObject()方法例項化bean之前。

2.2 setter迴圈依賴處理

  這個表示通過setter注入方式構成的迴圈依賴。對於setter注入造成的迴圈依賴Spring是通過提前暴露剛完成構造器注入但還未完成其他步驟(如setter注入)的bean來完成的,而且只能解決單例作用域的bean迴圈程式碼,我們這裡來詳細分析一下Spring是如何處理的。

  關於這部分的處理邏輯,在AbstractAutowireCapableBeanFactory的doCreateBean()方法中有一段程式碼,如下所示:

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    if (logger.isDebugEnabled()) {
        logger.debug("Eagerly caching bean '" + beanName +
                "' to allow for resolving potential circular references");
    }
    // 為避免後期迴圈依賴,可以在bean初始化完成前將建立例項的ObjectFactory加入工廠
    addSingletonFactory(beanName, new ObjectFactory<Object>() {
        public Object getObject() throws BeansException {
            // 對bean再一次依賴引用,主要應用SmartInstantiationAwareBeanPostProcessor,
            // 其中我們熟知的AOP就是在這裡將advice動態織入bean中,若沒有則直接返回bean,不做任何處理
            return getEarlyBeanReference(beanName, mbd, bean);
        }
    });
}

  這段程式碼不是很複雜,但是如果是一開始看這段程式碼的時候不太容易理解其作用,因為僅僅從函式中去理解是很難弄懂其中的含義,這裡需要從全域性的角度去思考Spring的依賴解決辦法才能更好理解。

  • earlySingletonExposure:從字面的意思理解就是是否提早曝光單例
  • mbd.isSingleton():是否是單例
  • this.allowCircularReference:是否允許迴圈依賴,在AbstractRefreshableApplicationContext中提供了設定函式,可以通過硬編碼的方式進行設定或者可以通過自定義名稱空間進行配置,硬編碼的方式程式碼如下:
ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext("aspectTest.xml");
bf.setAllowBeanDefinitionOverriding(false);
  • isSingletonCurrentlyInCreation(beanName):該bean是否在建立中。在Spring中,會有一個專門的屬性(類DefaultSingletonBeanRegistry中的singletonsCurrentlyInCreation)來記錄bean的載入狀態,在bean開始建立前會將beanName記錄在屬性中,在bean建立結束後會將beanName從屬性中移除。我們跟隨程式碼一路走來或許對這個屬性的記錄並沒有多少印象,不經會拍腦門問這個狀態是在哪裡記錄的呢?不同scope的記錄位置並不一樣,我們以singleton為例,在singleton下記錄屬性的函式是在DefaultSingletonBeanRegistry類的getSingleton(String beanName,ObjectFactory singletonFactory)函式中的beforeSingletonCreation(beanName)和afterSingletonCreation(beanName)中,在這兩段函式中分別通過this.singlesCurrentlyInCreation.add(beanName)與this.singlesCurrentlyInCreation.remove(beanName)來進行狀態的記錄與移除。

  經過上面的分析可以知道變數earlySingletonExposure為是否是單例、是否允許迴圈依賴、是否對應的bean正在建立這三個條件的綜合。當這3個條件都滿足時會執行addSingletonFactory操作,那麼加入SingletonFactory的作用又是什麼呢?

  這裡還是用一個最簡單的AB迴圈依賴為例,類A中含有屬性類B,而類B中又會含有屬性類A,那麼初始化beanA的過程如下圖所示:

  上圖展示了建立beanA的流程,圖中我們看到,在建立A的時候首先會記錄類A所對應的beanName,並將beanA的建立工廠加入快取中,而在對A的屬性填充也就是呼叫populate()方法的時候又會再一次的對B進行遞迴建立。同樣的,因為在B中同樣存在A屬性,因此在例項化B時的populate()方法中又會再次地初始化A,也就是圖形的最後,呼叫getBean(A)。關鍵就是在這裡,在這個getBean()函式中並不是直接去例項化A,而是先去檢測快取中是否有已經建立好的對應bean,或者是否有已經建立好的ObjectFactory,而此時對於A的ObjectFactory我們早已建立好了,所以便不會再去向後執行,而是直接呼叫ObjectFactory去獲取A。

  到這裡基本可以理清Spring處理迴圈依賴的解決辦法,這裡再從程式碼層面總結一下:

  在建立bean的過程中,例項化bean結束之後,屬性注入之前,有一段這樣的程式碼(程式碼位置為AbstractAutowireCapableBeanFactory類中的doCreateBean()方法中bean例項化之後):

    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        if (logger.isDebugEnabled()) {
            logger.debug("Eagerly caching bean '" + beanName +
                    "' to allow for resolving potential circular references");
        }
        addSingletonFactory(beanName, new ObjectFactory<Object>() {
            public Object getObject() throws BeansException {
                return getEarlyBeanReference(beanName, mbd, bean);
            }
        });
    }

  這段程式碼前面也說過,主要做的事情是在addSingletonFactory()方法中,即在必要的時候將建立bean的ObjectFactory新增到快取中。再結合前面的例子來看,在第一次建立beanA時,這裡是會將ObjectFactory加入到singletonFactories中,當建立beanB時,在對beanB的屬性注入時又會呼叫getBean()去獲取beanA,同樣是前面說到過,會先去快取獲取beanA,這時候是可以獲取到剛才放到快取中的ObjectFactory的,這時候就會把例項化好但是還未完成屬性注入的beanA找出來注入到beanB中去,這樣就解決了迴圈依賴的問題,需要結合下面的程式碼細品一下。

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

    final String beanName = transformedBeanName(name);
    Object bean;

    // Eagerly check singleton cache for manually registered singletons.
    Object sharedInstance = getSingleton(beanName);

    ...
}

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) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

2.3 prototype範圍的依賴處理

  對於"prototype"作用域的bean,Spring容器並不會對其進行快取,因此無法提前暴露一個建立中的bean,所以也是通過丟擲異常的方式來處理迴圈依賴,這裡仍然是用一個demo來測試一下程式碼是在哪拋的異常。

  配置檔案:

<bean id = "testA" class = "xxx" scope = "prototype">
    <property name = "testB" ref = "testB"/>
</bean>
<bean id = "testB" class = "xxx">
    <property name = "testC" ref = "testC"/>
</bean>
<bean id = "testC" class = "xxx">
    <property name = "testA" ref = "testA"/>
</bean>

  測試程式碼:

public static void main(String[] args) {
        try{
            ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
            System.out.println(ctx.getBean("testA"));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

  同樣通過斷點我們可以定位異常的丟擲位置是在AbstractBeanFactory類的doGetBean方法中,在方法開始獲取快取失敗之後(prototype不會加入到快取中),會首先判斷prototype的bean是否已建立,如果是就認為存在迴圈依賴,丟擲BeanCurrentlyInCreationException異常。

if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
}

 

3. 總結

  Spring中對於迴圈依賴的處理存在3中場景:

  • 構造器迴圈依賴處理;
  • setter迴圈依賴處理;
  • prototype範圍的依賴處理;

  其中對於構造器和prototype範圍的迴圈依賴,Spring是直接丟擲異常。而對於單例的setter迴圈依賴,Spring是通過在bean載入過程中提前將bean的ObjectFactory加入到singletonFactories這個快取用的map中來解決迴圈依賴的。

 

相關文章