Spring原始碼分析:Spring的迴圈依賴分析

農碼一生發表於2019-01-19

引言

  1. 基於Spring5+
  2. 什麼是迴圈依賴?
  3. 迴圈依賴有幾種?
  4. Spring可以解決哪幾種,為什麼不能解決這幾種?
  5. Spring是如何判斷存在迴圈依賴的?

什麼是迴圈依賴?

什麼是迴圈依賴?我們都知道Spring最大的作用就是來替我們管理Bean的,當然也包括Bean的建立以及整個生命週期,但是有這麼一種情況,假設有三個類A、B、C需要交給Spring來管理,但A例項的建立需要先有B例項,而B例項的建立需要先有C例項,C例項的建立需要先有A例項,這樣三個類就自然形成了一個環狀結構,如果用程式碼來表示,如下:

public class TestA {
    TestB testB;
    get;
    set;
}

public class TestB {
    TestC testC;
    get;
    set;
}

public class TestC {
    TestA testA;
    get;
    set;
}

這樣,三個類就彼此形成了一個環狀,那麼Spring是如何來處理這樣的狀況呢?

迴圈依賴有幾種?

有三種情況:

  1. 基於構造方法的迴圈依賴
  2. 基於setter構造的迴圈依賴(網上也叫field屬性依賴)
  3. 基於prototype範圍的依賴

Spring可以解決哪些迴圈依賴,為什麼?

首先說一下結論:除了第二種Spring可以幫我們解決,其它兩種都不能解決。我們知道Spring為我們完全例項化好一個Bean一定會經過一下三步:

  1. createBeanInstance:例項化,其實也就是呼叫物件的構造方法例項化物件。
  2. populateBean:填充屬性,這一步主要是多bean的依賴屬性進行填充。
  3. initializeBean:呼叫預設的或者自定義的init方法。

迴圈依賴的產生定會發生在步驟1和2中,因為1是利用構造方法,2是利用屬性賦值。

基於構造方法的迴圈依賴

先說結論基於構造器的迴圈依賴Spring是無法解決的,是因為沒有加入提前曝光的集合中,加入集合的條件是已經建立了Bean的包裝物件,而構造注入的時候,並沒有完成物件的建立,下面會有程式碼說明。

測試用例:

xml檔案:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA">
    <constructor-arg index="0" ref="testB"/>
</bean>

<bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB">
    <constructor-arg index="0" ref="testC"/>
</bean>

<bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC">
    <constructor-arg index="0" ref="testA"/>
</bean>

測試類:

/**
 * description:測試通過有參構造方式注入產生的迴圈依賴問題
 * @author 70KG
 * @date 2018/12/21
 */
public class Test02 {

    @Test
    public void m1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test02.xml");
    }

}

分析上面程式碼:

  1. Spring容器建立testA的Bean例項,首先去”當前建立Bean池”,查詢是否當前Bean正在建立,如果沒發現,則繼續準備其需要的構造器引數testB,並將testA識別符號放到”當前建立Bean池”。
  2. Spring容器建立testB的Bean例項,首先去”當前建立Bean池”,查詢是否當前Bean正在建立,如果沒發現,則繼續準備其需要的構造器引數testC,並將testB識別符號放到”當前建立Bean池”。
  3. Spring容器建立testC的Bean例項,首先去”當前建立Bean池”,查詢是否當前Bean正在建立,如果沒發現,則繼續準備其需要的構造器引數testA,並將testC識別符號放到”當前建立Bean池”。
  4. 到此為止Spring容器要去建立testA,但發現該Bean的標誌符在”當前建立Bean池”中,表示了迴圈依賴,於是丟擲BeanCurrentlyInCreationException異常。

其中”當前建立Bean池”就是一個Set集合,DefaultSingletonBeanRegistry類中beforeSingletonCreation方法,程式碼如下:

protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

然後我們來到建立Bean例項的地方:

AbstractAutowireCapableBeanFactory類的543行,通過這個方法返回一個這個Bean的包裝物件:

–> instanceWrapper = createBeanInstance(beanName, mbd, args);—-> 進入這個方法

–> AbstractAutowireCapableBeanFactory類的1129行

// Need to determine the constructor...
// 需要確定建構函式,也就是說構造方法的迴圈依賴會在這兒return
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null ||
        mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
        mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {
    return autowireConstructor(beanName, mbd, ctors, args);
}

// No special handling: simply use no-arg constructor.
// 無需特殊處理,僅使用無參構造即可,setter的迴圈依賴會在這個地方return
return instantiateBean(beanName, mbd);

在上面程式碼中返回Bean的包裝物件下面緊接著才是將這個物件曝光,也就是加入到SingletonFactory集合中,所以構造方法的迴圈引用,Spring是無法解決的,來到AbstractAutowireCapableBeanFactory的574行。

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

基於setter構造的迴圈依賴

首先說結論:Spring是可以為我們解決這樣的依賴的,原理說白了就是用了快取處理,也就是常說的提前曝光,為什麼叫提前曝光呢?因為這個快取中的Bean是一個還未進行賦值的Bean,僅僅是一個引用而已。

xml檔案:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA">
    <property name="loopB" ref="testB"/>
</bean>

<bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB">
    <property name="loopC" ref="testC"/>
</bean>

<bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC">
    <property name="loopA" ref="testA"/>
</bean>

測試類:

/**
 * description:通過setter注入產生的迴圈依賴問題
 * @author 70KG
 */
public class Test03 {
    @Test
    public void m1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test03.xml");
    }
}

程式碼分析:

  1. Spring容器建立單例”loopA”,首先根據無參構造建立Bean,並暴露到Map(singletonFactories)中,並將”loopA”標誌符放到當前建立正在建立的Bean池(singletonsCurrentlyInCreation)中,然後進行setter注入”loopB”。
  2. Spring容器建立單例”loopB”,首先根據無參構造建立Bean,並暴露到Map(singletonFactories)中,並將”loopA”標誌符放到當前建立正在建立的Bean池(singletonsCurrentlyInCreation)中,然後進行setter注入”loopC”。
  3. Spring容器建立單例”loopC”,首先根據無參構造建立Bean,並暴露到Map(singletonFactories)中,並將”loopA”標誌符放到當前建立正在建立的Bean池(singletonsCurrentlyInCreation)中,然後進行setter注入”loopA”。在注入”loopA”的時候,由於提前暴露在singletonFactories集合中了,利用它就可以取到”loopA”正在建立的Bean物件。
  4. 最後依賴注入”testB”,”testA”,完成setter注入。

檢視控制檯輸出日誌:

// 正在建立testA物件
Creating shared instance of singleton bean `testA`
Creating instance of bean `testA`
// 在快取早期引用,目的是防止迴圈引用問題
Eagerly caching bean `testA` to allow for resolving potential circular references
Creating shared instance of singleton bean `testB`
Creating instance of bean `testB`
Eagerly caching bean `testB` to allow for resolving potential circular references
Creating shared instance of singleton bean `testC`
Creating instance of bean `testC`
Eagerly caching bean `testC` to allow for resolving potential circular references
// 在建立testC的時候會去快取中拿原來儲存的testA,並返回,但此時的testA是一個不完全的物件,也就是尚未初始化
Returning eagerly cached instance of singleton bean `testA` that is not fully initialized yet - a consequence of a circular reference
// 緊接著完成C的建立,順便其它的也完成了
Finished creating instance of bean `testC`
Finished creating instance of bean `testB`
Finished creating instance of bean `testA`
Returning cached instance of singleton bean `testB`
Returning cached instance of singleton bean `testC`

基於setter的迴圈依賴利用了提前曝光機制,這一步的關鍵程式碼,在AbstractAutowireCapableBeanFactory的574行,程式碼如下:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

在加入SingletonFactory的前提是此Bean已經建立出來,才能夠加入到這個Map集合中,也就是提前曝光,可以讓別的Bean在初始化的時候從中拿到。否則是沒有機會加入到Map中的。

基於prototype範圍的依賴

首先說結論,對於多例情況下的迴圈依賴,是無法解決的,因為Spring容器不進行快取,更無法提前暴露。

測試用例:

xml檔案:

<bean id="testA" class="com.nmys.story.springCore.loop_dependency.loop01.LoopA" scope="prototype">
    <property name="loopB" ref="testB"/>
</bean>

<bean id="testB" class="com.nmys.story.springCore.loop_dependency.loop01.LoopB" scope="prototype">
    <property name="loopC" ref="testC"/>
</bean>

<bean id="testC" class="com.nmys.story.springCore.loop_dependency.loop01.LoopC" scope="prototype">
    <property name="loopA" ref="testA"/>
</bean>

測試類:

/**
 * description:通過setter注入產生的迴圈依賴問題
 * @author 70KG
 */
public class Test03 {

    @Test
    public void m1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test03.xml");
        LoopA loopA = context.getBean(LoopA.class);
        System.out.println(loopA);
    }

}

會丟擲BeanCurrentlyInCreationException異常。

Spring是如何檢測迴圈依賴

來到AbstractBeanFactory的246行,程式碼如下:

Object sharedInstance = getSingleton(beanName);

這一步是從快取中獲取以前建立的例項,如果發現存在,那麼就存在迴圈依賴。

到此,全文完,自我感覺比其他的整理還算詳細,如有疑問,請留言。

相關文章