引言
- 基於Spring5+
- 什麼是迴圈依賴?
- 迴圈依賴有幾種?
- Spring可以解決哪幾種,為什麼不能解決這幾種?
- 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是如何來處理這樣的狀況呢?
迴圈依賴有幾種?
有三種情況:
- 基於構造方法的迴圈依賴
- 基於setter構造的迴圈依賴(網上也叫field屬性依賴)
- 基於prototype範圍的依賴
Spring可以解決哪些迴圈依賴,為什麼?
首先說一下結論:除了第二種Spring可以幫我們解決,其它兩種都不能解決。我們知道Spring為我們完全例項化好一個Bean一定會經過一下三步:
- createBeanInstance:例項化,其實也就是呼叫物件的構造方法例項化物件。
- populateBean:填充屬性,這一步主要是多bean的依賴屬性進行填充。
- 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");
}
}
分析上面程式碼:
- Spring容器建立testA的Bean例項,首先去”當前建立Bean池”,查詢是否當前Bean正在建立,如果沒發現,則繼續準備其需要的構造器引數testB,並將testA識別符號放到”當前建立Bean池”。
- Spring容器建立testB的Bean例項,首先去”當前建立Bean池”,查詢是否當前Bean正在建立,如果沒發現,則繼續準備其需要的構造器引數testC,並將testB識別符號放到”當前建立Bean池”。
- Spring容器建立testC的Bean例項,首先去”當前建立Bean池”,查詢是否當前Bean正在建立,如果沒發現,則繼續準備其需要的構造器引數testA,並將testC識別符號放到”當前建立Bean池”。
- 到此為止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");
}
}
程式碼分析:
- Spring容器建立單例”loopA”,首先根據無參構造建立Bean,並暴露到Map(singletonFactories)中,並將”loopA”標誌符放到當前建立正在建立的Bean池(singletonsCurrentlyInCreation)中,然後進行setter注入”loopB”。
- Spring容器建立單例”loopB”,首先根據無參構造建立Bean,並暴露到Map(singletonFactories)中,並將”loopA”標誌符放到當前建立正在建立的Bean池(singletonsCurrentlyInCreation)中,然後進行setter注入”loopC”。
- Spring容器建立單例”loopC”,首先根據無參構造建立Bean,並暴露到Map(singletonFactories)中,並將”loopA”標誌符放到當前建立正在建立的Bean池(singletonsCurrentlyInCreation)中,然後進行setter注入”loopA”。在注入”loopA”的時候,由於提前暴露在singletonFactories集合中了,利用它就可以取到”loopA”正在建立的Bean物件。
- 最後依賴注入”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);
這一步是從快取中獲取以前建立的例項,如果發現存在,那麼就存在迴圈依賴。
到此,全文完,自我感覺比其他的整理還算詳細,如有疑問,請留言。