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

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

 

1. 什麼是循環依賴

  無論以前是否研究過循環依賴,這裏先對這個知識作一點回顧。緩存

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

 

  瞭解了什麼是循環依賴以後,咱們知道這是一種不可避免會出現的狀況,那做爲Bean容器的Spring又是怎麼處理這一問題呢?咱們接着往下看。測試

 

2. Spring如何處理循環依賴

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

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種狀況:this

2.1 構造器循環依賴處理

  這表示經過構造器注入構成的循環依賴,此依賴是沒法解決的,只能拋出BeanCurrentlyInCreationException異常表示循環依賴。編碼

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

  Spring容器將每個正在建立的bean標識符放在一個「當前建立bean池」中,bean標識符在建立過程當中將一直保持在這個池中,所以若是在建立bean的過程當中發現本身已經在「當前建立bean池」裏時,則拋出BeanCurrentlyInCreationException異常表示出現了循環依賴;而對於建立完畢的bean將從「當前建立bean池」中清除掉,這個「當前建立bean池」其實是一個ConcurrentHashMap,即DefaultSingletonBeanRegistry中的singletonsCurrentlyInCreation。prototype

  咱們經過一個直觀的測試用例來進行分析:debug

  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中來解決循環依賴的。

相關文章
相關標籤/搜索