Spring 循环引用(一)一个循环依赖引发的 BUG
在使用 Spring 的场景中,有时会碰到如下的一种情况,即 bean 之间的循环引用。即两个 bean 之间互相进行引用的情况。这时,在 Spring xml 配置文件中,就会出现如下的配置:
<bean id="beanA" class="BeanA" p:beanB-ref="beanB" />
<bean id="beanB" class="BeanB" p:beanA-ref="beanA" />
在一般情况下,这个配置在 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 循环依赖有以下几种情况:
- 多例 bean 循环依赖,Spring 无法解决,直接抛出异常。
- 单例 bean 通过构造器循环依赖,Spring 无法解决,直接抛出异常。
- 单例 bean 通过属性注入循环依赖,Spring 正常场景下可以处理这循环依赖的问题。本文讨论的正是这种情况。
一、模拟异常场景
(1) 存在两个 bean 相互依赖
public class BeanA {
private BeanB beanB;
}
public class BeanB {
private BeanA beanA;
}
(2) xml 配置
<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 可以正确处理循环依赖。
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 有以下几个主要的步骤:
-
createBeanInstance
实例化 bean 对象,一般是通过反射调用默认的构造器。 -
populateBean
bean 属性注入,在这个步骤会从 Spring 容器中查找对应属性字段的值,解决循环依赖问题。 -
initializeBean
调用的 bean 定义的初始化方法。
Spring 解决循环思路是第一步创建 bean 实例后,就将这个未进行属性注入的 bean 通过 addSingletonFactory 添加到 beanFactory 的容器中,这样即使这个对象还未创建完成就可以通过 getSingleton(beanName) 直接在容器中找到这个 bean。过程如下所示:
上图展示了创建 beanA 的流程,毫无疑问在 beanA 实例化完成后通过 addSingletonFactory 将这个还未初始化的对象暴露到容器后,就可以通过 getBean(A) 查找到了,这样可以解决依赖的问题了。但就真的没有问题了吗?Spring 又为什么要抛出上述 BeanCurrentlyInCreationException 的异常呢?
- 如果是通过构造器循环依赖,则 beanA 根本无法实例化,也就不存在提前暴露到 Spring 容器一说了。所以 Spring 根本就不支持通过构造器的循环依赖。
- 多例或其它类型的 bean 根本就不归 Spring 容器管理,因此也不支持这种循环注入的问题。
- 如果 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");
}