Spring 源碼學習(五) 循環依賴

還記得上一篇筆記,在 bean 加載流程,在創建過程中,出現了依賴循環的監測,如果出現了這個循環依賴,而沒有解決的話,代碼中將會報錯,然後 Spring 容器初始化失敗。

由於感覺循環依賴是個比較獨立的知識點,所以我將它的分析單獨寫一篇筆記,來看下什麼是循環依賴和如何解決它。


Table of Contents generated with DocToc

  • 前言
  • 循環依賴構造器循環依賴property 範圍的依賴處理setter 循環依賴代碼分析解決場景結合關鍵代碼梳理流程創建原始 beanaddSingleFactorypopulateBean 填充屬性getSingleton
  • 總結
  • 參考資料

循環依賴

循環依賴就是循環引用,就是兩個或者多個 bean 相互之間的持有對方,最後形成一個環。例如 A 引用了 B,B 引用了 C,C 引用了 A。

可以參照下圖理解(圖中展示的類的互相依賴,但循環調用指的是方法之間的環調用,下面代碼例子會展示方法環調用):

Spring 源碼學習(五) 循環依賴

如果學過數據庫的同學,可以將循環依賴簡單的理解為死鎖,互相持有對方的資源,形成一個環,然後不釋放資源,導致死鎖發生。

在循環調用中,除非出現終結條件,否則將會無限循環,最後導致內存溢出錯誤。(我也遇到過一次 OOM,也是無限循環導致的)


書中的例子是用了三個類進行環調用,我為了簡單理解和演示,使用了兩個類進行環調用:

在 Spring 中,循環依賴分為以下三種情況:

構造器循環依賴


Spring 源碼學習(五) 循環依賴


通過上圖的配置方法,在初始化的時候就會拋出 BeanCurrentlyInCreationException 異常

<code>public static void main(String[] args) {// 報錯原因: Requested bean is currently in creation: Is there an unresolvable circular reference?ApplicationContext context = new ClassPathXmlApplicationContext("circle/circle.xml");}/<code>

從上一篇筆記中知道,Spring 容器將每一個正在創建的 bean 標識符放入一個 “當前創建 bean 池(prototypesCurrentlyInCreation)” 中,bean 標識符在創建過程中將一直保持在這個池中。

檢測循環依賴的方法:

分析上面的例子,在實例化 circleA 時,將自己 A 放入池中,由於依賴了 circleB,於是去實例化 circleB,B 也放入池中,由於依賴了 A,接著想要實例化 A,發現在創建 bean 過程中發現自己已經在 “當前創建 bean” 裡時,於是就會拋出 BeanCurrentlyInCreationException 異常。

如圖中展示,這種通過構造器注入的循環依賴,是無法解決的


property 範圍的依賴處理

property 原型屬於一種作用域,所以首先來了解一下作用域 scope 的概念:

在 Spring 容器中,在Spring容器中是指其創建的 Bean 對象相對於其他 Bean 對象的請求可見範圍

我們最常用到的是單例 singleton 作用域的 bean,Spring 容器中只會存在一個共享的 Bean 實例,所以我們每次獲取同樣 id 時,只會返回bean的同一實例。

使用單例的好處有兩個:

  1. 提前實例化 bean,將有問題的配置問題提前暴露
  2. 將 bean 實例放入單例緩存 singletonFactories 中,當需要再次使用時,直接從緩存中取,加快了運行效率。

單一實例會被存儲在單例緩存 singletonFactories 中,為Spring的缺省作用域.

看完了單例作用域,來看下 property 作用域的概念:在 Spring 調用原型 bean 時,每次返回的都是一個新對象,相當於 new Object()。

因為 Spring 容器對原型作用域的 bean 是不進行緩存,因此無法提前暴露一個創建中的 bean,所以也是無法解決這種情況的循環依賴。


setter 循環依賴

對於 setter 注入造成的依賴可以通過 Spring 容器提前暴露剛完成構造器注入但未完成其他步驟(如 setter 注入)的 bean 來完成,而且只能解決單例作用域的 bean 依賴。

在類的加載中,核心方法 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean,在這一步中有對循環依賴的校驗和處理。

跟進去方法能夠發現,如果 bean 是單例,並且允許循環依賴,那麼可以通過提前暴露一個單例工廠方法,從而使其他 bean 能引用到,最終解決循環依賴的問題。

還是按照上面新建的兩個類, CircleA 和 CircleB,來講下 setter 解決方法:

配置:

<code><bean><property>/<bean><bean><property>/<bean>/<code>

執行 Demo 和輸出:

<code>public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("circle/circle.xml");CircleA circleA = (CircleA) context.getBean("circleA");circleA.a();}在 a 方法中,輸出 A,在 b 方法中,輸出B,下面是執行 demo 輸出的結果:錯誤提示是因為兩個方法互相調用進行輸出,然後打印到一定行數提示 main 函數棧溢出了=-=ABAB*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844Exception in thread "main" java.lang.StackOverflowError/<code>

可以看到通過 setter 注入,成功解決了循環依賴的問題,那解決的具體代碼是如何實現的呢,下面來分析一下:


代碼分析

為了更好的理解循環依賴,首先來看下這三個變量(也叫緩存,可以全局調用的)的含義和用途:

<code>/** Cache of singleton objects: bean name to bean instance. */private final Map<string> singletonObjects = new ConcurrentHashMap<>(256);/** Cache of singleton factories: bean name to ObjectFactory. */private final Map<string>> singletonFactories = new HashMap<>(16);/** Cache of early singleton objects: bean name to bean instance. */private final Map<string> earlySingletonObjects = new HashMap<>(16);/<string>/<string>/<string>/<code>

變量用途singletonObjects用於保存 BeanName 和創建 bean 實例之間的關係,bean-name –> instanctsingletonFactories用於保存 BeanName 和創建 bean 的 工廠 之間的關係,bean-name –> objectFactoryearlySingletonObjects也是保存 beanName 和創建 bean 實例之間的關係,與 singletonObjects 的不同之處在於,當一個單例 bean 被放入到這裡之後,那麼其他 bean 在創建過程中,就能通過 getBean 方法獲取到,目的是用來檢測循環引用


之前講過類加載的機制了,下面定位到創建 bean 時,解決循環依賴的地方:

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean


<code>// 是否需要提前曝光,用來解決循環依賴時使用boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {if (logger.isTraceEnabled()) {logger.trace("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}// 註釋 5.2 解決循環依賴 第二個參數是回調接口,實現的功能是將切面動態織入 beanaddSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}protected void addSingletonFactory(String beanName, ObjectFactory> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {// 判斷 singletonObjects 不存在 beanNameif (!this.singletonObjects.containsKey(beanName)) {// 註釋 5.4 放入 beanName -> beanFactory,到時在 getSingleton() 獲取單例時,可直接獲取創建對應 bean 的工廠,解決循環依賴this.singletonFactories.put(beanName, singletonFactory);// 從提前曝光的緩存中移除,之前在 getSingleton() 放入的this.earlySingletonObjects.remove(beanName);// 往註冊緩存中添加 beanNamethis.registeredSingletons.add(beanName);}}}/<code> 

先來看 earlySingletonExposure 這個變量: 從字面意思理解就是需要提前曝光的單例

有以下三個判斷條件:

  • mbd 是否是單例
  • 該容器是否允許循環依賴
  • 判斷該 bean 是否在創建中。

如果這三個條件都滿足的話,就會執行 addSingletonFactory 操作。要想著,寫的代碼都有用處,所以接下來看下這個操作解決的什麼問題和在哪裡使用到吧


解決場景

用一開始創建的 CircleA 和 CircleB 這兩個循環引用的類作為例子:


Spring 源碼學習(五) 循環依賴


A 類中含有屬性 B,B 類中含有屬性 A,這兩個類在初始化的時候經歷了以下的步驟:

  1. 創建 beanA,先記錄對應的 beanName 然後將 beanA 的創建工廠 beanFactoryA 放入緩存中
  2. 對 ` beanA 的屬性填充方法 populateBean,檢查到依賴 beanB,緩存中沒有 beanB 的實例或者單例緩存,於是要去實例化 beanB`。
  3. 開始實例化 beanB,經歷創建 beanA 的過程,到了屬性填充方法,檢查到依賴了 beanA。
  4. 調用 getBean(A) 方法,在這個函數中,不是真正去實例化 beanA,而是先去檢測緩存中是否有已經創建好的對應的 bean,或者已經創建好的 beanFactory
  5. 檢測到 beanFactoryA 已經創建好了,而是直接調用 ObjectFactory 去創建 beanA

結合關鍵代碼梳理流程

創建原始 bean

<code>BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);// 原始 beanfinal Object bean = instanceWrapper.getWrappedInstance();/<code>

在這一步中,創建的是原始 bean,因為還沒到最後一步屬性解析,所以這個類裡面沒有屬性值,可以將它想象成 new ClassA,同時沒有構造函數等賦值的操作,這個原始 bean 信息將會在下一步使用到。


addSingleFactory

<code>// 註釋 5.2 解決循環依賴 第二個參數是回調接口,實現的功能是將切面動態織入 beanaddSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));/<code>

前面也提到過這個方法,它會將需要提前曝光的單例加入到緩存中,將單例的 beanName 和 beanFactory 加入到緩存,在之後需要用到的時候,直接從緩存中取出來。


populateBean 填充屬性

剛才第一步時也說過了,一開始創建的只是初始 bean,沒有屬性值,所以在這一步會解析類的屬性。在屬性解析時,會判斷屬性的類型,如果判斷到是 RuntimeBeanReference 類型,將會解析引用。

就像我們寫的例子,CircleA 引用了 CircleB,在加載 CircleA時,發現 CircleB 依賴,於是乎就要去加載 CircleB。

我們來看下代碼中的具體流程吧:

<code>protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {    ...    if (pvs != null) {// 將屬性應用到 bean 中,使用深拷貝,將子類的屬性一併拷貝applyPropertyValues(beanName, mbd, bw, pvs);}}protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {    ...    String propertyName = pv.getName();Object originalValue = pv.getValue();// 註釋 5.5 解析參數,如果是引用對象,將會進行提前加載Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);...}public Object resolveValueIfNecessary(Object argName, @Nullable Object value) {// 我們必須檢查每個值,看看它是否需要一個運行時引用,然後來解析另一個 beanif (value instanceof RuntimeBeanReference) {// 註釋 5.6 在這一步中,如果判斷是引用類型,需要解析引用,加載另一個 beanRuntimeBeanReference ref = (RuntimeBeanReference) value;return resolveReference(argName, ref);}...}/<code> 

跟蹤到這裡,加載引用的流程比較清晰了,發現是引用類的話,最終會委派 org.springframework.beans.factory.support.BeanDefinitionValueResolver#resolveReference 進行引用處理,核心的兩行代碼如下:

<code>// 註釋 5.7 在這裡加載引用的 beanbean = this.beanFactory.getBean(refName);this.beanFactory.registerDependentBean(refName, this.beanName);/<code>

在這一步進行 CircleB 的加載,但是我們寫的例子中,CircleB 依賴了 CircleA,那它是如何處理的呢,所以這時,我們剛才將 CircleA 放入到緩存中的信息就起到了作用。


getSingleton

還記得之前在類加載時學到的只是麼,單例模式每次加載都是取同一個對象,如果在緩存中有,可以直接取出來,在緩存中沒有的話才進行加載,所以再來熟悉一下取單例的方法:

<code>protected Object getSingleton(String beanName, boolean allowEarlyReference) {Object singletonObject = this.singletonObjects.get(beanName);// 檢查緩存中是否存在實例if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {// 記住,公共變量都需要加鎖操作,避免多線程併發修改synchronized (this.singletonObjects) {// 如果此 bean 正在加載則不處理singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {// 當某些方法需要提前初始化,調用 addSingletonFactory 方法將對應的// objectFactory 初始化策略存儲在 earlySingletonObjects,並且從 singletonFactories 移除ObjectFactory> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}return singletonObject;}/<code>

雖然 CircleB 引用了 CircleA,但在之前的方法 addSingletonFactory 時,CircleA 的 beanFactory 就提前暴露。

所以 CircleB 在獲取單例 getSingleton() 時,能夠拿到 CircleA 的信息,所以 CircleB 順利加載完成,同時將自己的信息加入到緩存和註冊表中,接著返回去繼續加載 CircleA,由於它的依賴已經加載到緩存中,所以 CircleA 也能夠順利完成加載,最終整個加載操作完成~


分享到:


相關文章: