Spring原始碼之Bean的載入(三)

神祕傑克發表於2022-06-09

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池”移除。

  1. 建立配置檔案
<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>
  1. 建立測試用例
public class Test {
    public static void main(String[] args) {
         new ClassPathXmlApplicationContext("spring-config.xml");
    }
}
  1. 執行結果
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方法。

呼叫applyBeanPostProcessorsBeforeInitialization方法

applyBeanPostProcessorsBeforeInitialization

在getBeanPostProcessors中中有一個處理器為: AnnotationAwareAspectJAutoProxyCreator 其實就是加的註解切面,隨後呼叫會跳轉到 AbstractAutoProxyCreator 類的 postProcessAfterInitialization 方法中。

如下程式碼,wrapIfNecessary 方法會判斷是否滿足代理條件,是的話返回一個代理物件,否則返回當前 Bean。

postProcessAfterInitialization

最後TestA 被替換為了代理物件。在doCreateBean 返回,以及後面放到一級快取中的都是代理物件。

如果TestA和TestB都是用了AOP動態代理,前面的一些列流程,都和正常的沒有什麼區別。而唯一的區別在於,建立 TestB 的時候,需要從三級快取獲取 TestA。

此時在 getSingleton 方法中會呼叫:singletonObject = singletonFactory.getObject();

getSingleton

具體實現

getEarlyBeanReference

postProcessAfterInitialization

看到 wrapIfNecessary 就明白了吧!這裡會獲取一個代理物件

也就是說此時返回,並放到二級快取的是一個 TestA 的代理物件。

這樣 TestB 就建立完畢了!

到 TestA 開始初始化並執行後置處理器了,因為 TestA 也有代理,所以 TestA 也會執行到 postProcessAfterInitialization 這一部分!

但是在執行 wrapIfNecessary 之前,會先判斷代理物件快取是否有 TestA 了。

判斷是否存在代理物件

但是這塊獲取到的是 TestA 的代理物件。肯定是 false 。 所以不會再生成一次 TestA 的代理物件。

總結

TestA和TestB都存在動態代理情況下的流程圖:

存在動態代理迴圈依賴流程圖

相關文章