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


天天用心记录一点点。内容也许不重要,但习惯很重要!

相关文章
相关标签/搜索