Spring 循环引用(一)一个循环依赖引发的 BUG

Spring 循环引用(一)一个循环依赖引发的 BUG

Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html)

Spring 循环引用相关文章:

  1. 《Spring 循环引用(一)一个循环依赖引发的 BUG》:https://www.cnblogs.com/binarylei/p/10325698.html
  2. 《Spring 循环引用(二)源码分析》:https://www.cnblogs.com/binarylei/p/10326046.html

在使用 Spring 的场景中,有时会碰到如下的一种情况,即 bean 之间的循环引用。即两个 bean 之间互相进行引用的情况。这时,在 Spring xml 配置文件中,就会出现如下的配置:


在一般情况下,这个配置在 Spring 中是可以正常工作的,前提是没有对 beanA 和 beanB 进行增强。但是,如果任意一方进行了增强,比如通过 spring 的代理对 beanA 进行了增强,即实际返回的对象和原始对象不一致的情况,在这种情况下,就会报如下一个错误:

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 使用何种策略处理循环引用问题说起。

Spring 循环依赖有以下几种情况:

  1. 多例 bean 循环依赖,Spring 无法解决,直接抛出异常。
  2. 单例 bean 通过构造器循环依赖,Spring 无法解决,直接抛出异常。
  3. 单例 bean 通过属性注入循环依赖,Spring 正常场景下可以处理这循环依赖的问题。本文讨论的正是这种情况。

一、模拟异常场景

(1) 存在两个 bean 相互依赖

public class BeanA {
    private BeanB beanB;
}

public class BeanB {
    private BeanA beanA;
}

(2) xml 配置



    
    

(3) 正常场景

如果不对 BeanA 进行任务增强,Spring 可以正确处理循环依赖。

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 后置处理器,再次运行代码则会抛出上述异常。

二、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 循环引用(一)一个循环依赖引发的 BUG_第1张图片

上图展示了创建 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 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


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

你可能感兴趣的:(Spring 循环引用(一)一个循环依赖引发的 BUG)