Spring 循環引用(一)一個循環依賴引起的 BUG

Spring 循環引用(一)一個循環依賴引起的 BUG

Spring 系列目錄(http://www.javashuo.com/article/p-kqecupyl-bm.html)php

Spring 循環引用相關文章:html

  1. 《Spring 循環引用(一)一個循環依賴引起的 BUG》:http://www.javashuo.com/article/p-bwuysaxv-w.html
  2. 《Spring 循環引用(二)源碼分析》:http://www.javashuo.com/article/p-gqplicns-ce.html

在使用 Spring 的場景中,有時會碰到以下的一種狀況,即 bean 之間的循環引用。即兩個 bean 之間互相進行引用的狀況。這時,在 Spring xml 配置文件中,就會出現以下的配置:java

<bean id="beanA" class="BeanA" p:beanB-ref="beanB" />
<bean id="beanB" class="BeanB" p:beanA-ref="beanA" />

在通常狀況下,這個配置在 Spring 中是能夠正常工做的,前提是沒有對 beanA 和 beanB 進行加強。可是,若是任意一方進行了加強,好比經過 spring 的代理對 beanA 進行了加強,即實際返回的對象和原始對象不一致的狀況,在這種狀況下,就會報以下一個錯誤:git

Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'beanA': Bean with name 'beanA' has been injected into other beans [beanB] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:605)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at com.github.binarylei.spring.beans.factory.circle.Main.main(Main.java:13)

這個錯誤即對於一個 bean,其所引用的對象並非由 Spring 容器最終生成的對象,而只是一個原始對象,而 Spring 默認是不容許這種狀況出現,即持有過程當中間對象。那麼,這個錯誤是如何產生的,以及在 Spring 內部,是如何來檢測這種狀況的呢。這就得從 Spring 如何建立一個對象,以及如何處理 bean 間引用,以及 Spring 使用何種策略處理循環引用問題提及。github

Spring 循環依賴有如下幾種狀況:spring

  1. 多例 bean 循環依賴,Spring 沒法解決,直接拋出異常。
  2. 單例 bean 經過構造器循環依賴,Spring 沒法解決,直接拋出異常。
  3. 單例 bean 經過屬性注入循環依賴,Spring 正常場景下能夠處理這循環依賴的問題。本文討論的正是這種狀況。

1、模擬異常場景

(1) 存在兩個 bean 相互依賴app

public class BeanA {
    private BeanB beanB;
}

public class BeanB {
    private BeanA beanA;
}

(2) xml 配置ide

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="beanA" class="com.github.binarylei.spring.beans.factory.circle.BeanA" p:beanB-ref="beanB"/>
    <bean id="beanB" class="com.github.binarylei.spring.beans.factory.circle.BeanB" p:beanA-ref="beanA"/>
</beans>

(3) 正常場景源碼分析

若是不對 BeanA 進行任務加強,Spring 能夠正確處理循環依賴。post

public class Main {

    public static void main(String[] args) {
        XmlBeanFactory beanFactory = new XmlBeanFactory(
                new ClassPathResource("spring-context-circle.xml"));
        // beanFactory.addBeanPostProcessor(new CircleBeanPostProcessor());

        BeanA beanA = (BeanA) beanFactory.getBean("beanA");
    }
}

(4) 異常場景

如今對 BeanA 用 Spring 提供的 BeanPostProcessor 進行加強處理,這樣最終獲得的 beanA 就是代理後的對象了。

public class CircleBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean instanceof BeanA ? new BeanA() : bean;
    }
}

此時給 beanFactory 註冊一個 BeanPostProcessor 後置處理器,再次運行代碼則會拋出上述異常。

2、Spring 中的循環依賴

2.1 Spring 解決循環依賴的思路

在 Spring 中初始化一個單例的 bean 有如下幾個主要的步驟:

  1. createBeanInstance 實例化 bean 對象,通常是經過反射調用默認的構造器。
  2. populateBean bean 屬性注入,在這個步驟會從 Spring 容器中查找對應屬性字段的值,解決循環依賴問題。
  3. initializeBean 調用的 bean 定義的初始化方法。

Spring 解決循環思路是第一步建立 bean 實例後,就將這個未進行屬性注入的 bean 經過 addSingletonFactory 添加到 beanFactory 的容器中,這樣即便這個對象還未建立完成就能夠經過 getSingleton(beanName) 直接在容器中找到這個 bean。過程以下所示:

Spring 循環依賴解決思路

上圖展現了建立 beanA 的流程,毫無疑問在 beanA 實例化完成後經過 addSingletonFactory 將這個還未初始化的對象暴露到容器後,就能夠經過 getBean(A) 查找到了,這樣能夠解決依賴的問題了。但就真的沒有問題了嗎?Spring 又爲何要拋出上述 BeanCurrentlyInCreationException 的異常呢?

  1. 若是是經過構造器循環依賴,則 beanA 根本沒法實例化,也就不存在提早暴露到 Spring 容器一說了。因此 Spring 根本就不支持經過構造器的循環依賴。
  2. 多例或其它類型的 bean 根本就不歸 Spring 容器管理,所以也不支持這種循環注入的問題。
  3. 若是 beanA 在屬性注入完成後,也就是在第三步 initializeBean 又對 beanA 進行了加強,這樣會致使一個嚴重的問題,beanB 中持有的 beanA 是還未加強的,也就是說這兩個 beanA 不是同一個對象了。 Spring 默認是不容許這種狀況發生的,即 allowRawInjectionDespiteWrapping=false,固然咱們也能夠進行配置。

2.2 Bug 緣由分析

Spring 在 createBeanInstance、populateBean、initializeBean 完成 bean 的建立後,還有一個依賴檢查。以 beanA 的建立過程爲例(beanA -> beanB -> beanA)

// 1. earlySingletonExposure=true 時容許循環依賴
if (earlySingletonExposure) {
    // 2. 獲取容器中的提早暴露的 beanA 對象,這個對象只有在循環依賴時纔有值
    //    此時這個提早暴露的 beanA 被其依賴的對象持有 eg: beanB
    Object earlySingletonReference = getSingleton(beanName, false);
    if (earlySingletonReference != null) {
        // 3. exposedObject = initializeBean(beanName, exposedObject, mbd) 也就是說後置處理器可能對其作了加強
        //    這樣暴露先後的 beanA 可能再也不是同一個對象,Spring 默認是不容許這種狀況發生的
        //    也就是 allowRawInjectionDespiteWrapping=false
        // 3.1 beanA 沒有被加強
        if (exposedObject == bean) {
            exposedObject = earlySingletonReference;
        // 3.2 beanA 被加強
        //     若是存在依賴 beanA 的對象(eg: beanB),而且這個對象已經建立,則說明未被加強的 beanA 被其它對象依賴 
        } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            String[] dependentBeans = getDependentBeans(beanName);
            Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
            for (String dependentBean : dependentBeans) {
                // beanB 已經建立,則說明它依賴了未被加強的 beanA,這樣容器中實際存在兩個不一樣的 beanA 了
                if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                    actualDependentBeans.add(dependentBean);
                }
            }
            if (!actualDependentBeans.isEmpty()) {
                throw new BeanCurrentlyInCreationException(beanName,
                        "Bean with name '" + beanName + "' has been injected into other beans [" +
                        StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                        "] in its raw version as part of a circular reference, but has eventually been " +
                        "wrapped. This means that said other beans do not use the final version of the " +
                        "bean. This is often the result of over-eager type matching - consider using " +
                        "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
            }
        }
    }
}

簡單來講就是,beanA 還未初始化完成就將這個對象暴露到 Spring 容器中了,此時建立 beanB 時會經過 getBean(A) 獲取這個還未初始化完成的 beanA。若是此後 Spring 容器沒有修改 beanA 還好,但要是以後在第三步 initializeBean 又對 beanA 進行了加強的話,此時問題來了:Spring 容器實際上有兩個 beanA,加強前和加強後的。異常就此誕生。

固然 Spring 了提供了控制是否要校驗的參數 allowRawInjectionDespiteWrapping,默認爲 false,就是不容許這種狀況發生。

2.2 Bug 修復

知道了 BeanCurrentlyInCreationException 產生的緣由,那咱們能夠強行修復這個 Bug,固然最好的辦法是不要在代碼中出現循環依賴的場景。

public static void main(String[] args) {
    XmlBeanFactory beanFactory = new XmlBeanFactory(
            new ClassPathResource("spring-context-circle.xml"));
    beanFactory.addBeanPostProcessor(new CircleBeanPostProcessor());
    // 關鍵
    beanFactory.setAllowRawInjectionDespiteWrapping(true);

    BeanA beanA = (BeanA) beanFactory.getBean("beanA");
}

參考:

1 . 《Spring中循環引用的處理》:https://www.iflym.com/index.php/code/201208280001.html


天天用心記錄一點點。內容也許不重要,但習慣很重要!

相關文章
相關標籤/搜索