bean 的載入(三)
之前文章主要講解了從bean的例項中獲取物件,準備過程以及例項化的前置處理。例項化bean是一個非常複雜的過程,本文主要講解Spring是如何解決迴圈依賴。
什麼是迴圈依賴
迴圈依賴就是迴圈引用,其實就是兩個或者多個bean相互持有對方,比如 A 引用 B ,B 引用 C,C 引用 A,最終成為一個環。
迴圈依賴是無法解決的,除非有終結條件,否則就是死迴圈,直到記憶體溢位。
什麼情況下迴圈依賴可以被處理
Spring中解決迴圈依賴是有前置條件:
- 出現迴圈依賴的bean必須是單例的,如果是prototype則不會出現
- 依賴注入的方式不能全為構造器注入,只能解決純setter注入的情況
依賴情況 | 依賴注入方式 | 是否可以解決 |
---|---|---|
A、B相互依賴 | 均採用setter方式注入 | 可以 |
A、B相互依賴 | 均採用屬性自動注入 | 可以 |
A、B相互依賴 | 均採用構造器注入 | 不可以 |
A、B相互依賴 | A中注入為setter,B中為構造器 | 可以 |
A、B相互依賴 | A中注入為構造器,B中為setter,Spring在建立過程中會根據自然排序,A優先於B建立 | 不可以 |
Spring如何解決迴圈依賴
首先Spring容器迴圈依賴包括構造器迴圈依賴和setter迴圈依賴,在瞭解Spring是如何解決迴圈依賴之前,我們先建立這幾個類。
public class TestA {
private TestB testB;
public TestA(TestB testB) {
this.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 TestB(TestC testC) {
this.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 TestC(TestA testA) {
this.testA = testA;
}
public void c() {
testA.a();
}
public TestA getTestA() {
return testA;
}
public void setTestA(TestA testA) {
this.testA = testA;
}
}
構造器迴圈依賴
構造器迴圈依賴表示通過構造器注入造成的迴圈依賴,需要注意的是,這種情況是無法解決的,會丟擲BeanCurrentlyInCreationException
異常。
Spring容器會將每一個正在建立的Bean識別符號放在一個“當前建立Bean池”中,Bean識別符號在建立過程中將一直保持在這個池中。如果在建立bean的過程中發現自己已經在“當前建立Bean池”時,就會丟擲上述異常表示迴圈依賴。當bean建立完成後則會從“當前建立Bean池”移除。
- 建立配置檔案
<bean id="a" class="cn.jack.TestA">
<constructor-arg index="0" ref="b"/>
</bean>
<bean id="b" class="cn.jack.TestB">
<constructor-arg index="0" ref="c"/>
</bean>
<bean id="c" class="cn.jack.TestC">
<constructor-arg index="0" ref="a"/>
</bean>
- 建立測試用例
public class Test {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("spring-config.xml");
}
}
- 執行結果
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [spring-config.xml]: Cannot resolve reference to bean 'c' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'c' defined in class path resource [spring-config.xml]: Cannot resolve reference to bean 'a' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
如果瞭解了剛才描述的情況,我們很容易就可以想到是在建立TestC物件的時候需要準備其構造引數TestA,這時候Spring容器要去建立TestA,但發現該bean識別符號已經在“當前建立Bean池”中了,所以就丟擲上述異常。
setter迴圈依賴
對於setter注入造成的依賴是通過Spring容器提前暴露剛完成構造器注入但還未完成其他步驟(比如setter注入)的bean來完成的,而且只能解決單例作用域下的bean迴圈依賴。通過提前暴露一個單例工廠方法,從而使其他bean可以引用到該bean。
根據我們的程式碼案例流程如下:
- Spring容器建立單例TestA,隨後呼叫無參構造器建立bean,並暴露出ObjectFactory,用於返回一個提前暴露建立中的bean,並將 testA 識別符號放到 “當前建立bean池”中,然後進行setter注入 TestB
- Spring容器建立單例TestB,隨後呼叫無參構造器建立bean,並暴露出ObjectFactory,用於返回一個提前暴露建立中的bean,並將 testB 識別符號放到 “當前建立bean池”中,然後進行setter注入 TestC
- Spring容器建立單例TestC,隨後呼叫無參構造器建立bean,並暴露出ObjectFactory,用於返回一個提前暴露建立中的bean,並將 testC 識別符號放到 “當前建立bean池”中,然後進行setter注入 TestA ,由於之前已經提前暴露了 ObjectFactory,從而使用它返回提前暴露的一個建立中的bean。其餘同理。
prototype範圍的依賴處理
scope="prototype"意思是每次請求都會建立一個例項物件。
兩者的區別是:有狀態的bean都使用Prototype作用域,無狀態的一般都使用singleton單例作用域。
對於“prototype”作用域Bean,Spring容器無法完成依賴注入,因為“prototype”作用域的Bean,Spring容器不進行快取,因此無法提前暴露一個建立中的Bean。所以還是會丟擲上述異常。
流程解析
這裡就拿TestA 依賴 TestB,TestB 依賴 TestA 舉例。
在 TestA 和 TestB 迴圈依賴的場景中:
TestB populatedBean
查詢依賴項 TestA 的時候,從一級快取中雖然未獲取到 TestA,但是發現 TestA 在建立中。
此時,從三級快取中獲取 A 的 singletonFactory
呼叫工廠方法,建立 getEarlyBeanReference
TestA 的早期引用並返回。
二級快取能否解決迴圈依賴
我們知道在例項化過程中,將處於半成品的物件地址全部放在快取中,提前暴露物件,在後續的過程中,再次對提前暴露的物件進行賦值,然後將賦值完成的物件,也就是成品物件放在一級快取中,刪除二級和三級快取。
如果不要二級快取的話,一級快取會存在半成品和成品的物件,獲取的時候,可能會獲取到半成品的物件,無法使用。
如果不要三級快取的話,未使用AOP的情況下,只需要一級和二級快取也是可以解決Spring迴圈依賴;但是如果使用了AOP進行增強功能的話,必須使用三級快取,因為在獲取三級快取過程中,會用代理物件替換非代理物件,如果沒有三級快取,那麼就無法得到代理物件。
三級快取是為了解決AOP代理過程中產生的迴圈依賴問題。
我們在程式碼上增加aop相關切面操作後,變化就在initializeBean方法中產生,呼叫applyBeanPostProcessorsBeforeInitialization方法。
在getBeanPostProcessors中中有一個處理器為: AnnotationAwareAspectJAutoProxyCreator
其實就是加的註解切面,隨後呼叫會跳轉到 AbstractAutoProxyCreator 類的 postProcessAfterInitialization 方法
中。
如下程式碼,wrapIfNecessary 方法會判斷是否滿足代理條件,是的話返回一個代理物件,否則返回當前 Bean。
最後TestA 被替換為了代理物件。在doCreateBean 返回,以及後面放到一級快取中的都是代理物件。
如果TestA和TestB都是用了AOP動態代理,前面的一些列流程,都和正常的沒有什麼區別。而唯一的區別在於,建立 TestB 的時候,需要從三級快取獲取 TestA。
此時在 getSingleton
方法中會呼叫:singletonObject = singletonFactory.getObject();
看到 wrapIfNecessary
就明白了吧!這裡會獲取一個代理物件
。
也就是說此時返回,並放到二級快取的是一個 TestA 的代理物件。
這樣 TestB 就建立完畢了!
到 TestA 開始初始化並執行後置處理器了,因為 TestA 也有代理,所以 TestA 也會執行到 postProcessAfterInitialization
這一部分!
但是在執行 wrapIfNecessary
之前,會先判斷代理物件快取是否有 TestA 了。
但是這塊獲取到的是 TestA 的代理物件。肯定是 false 。 所以不會再生成一次 TestA 的代理物件。
總結
TestA和TestB都存在動態代理情況下的流程圖: