spring源碼閱讀筆記09:循環依賴

  前面的文章一直在研究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中來解決循環依賴的。