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 配置文件中,就会出现如下的配置:

<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 循环依赖有以下几种情况:

  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 配置

<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 有以下几个主要的步骤:

  1. createBeanInstance 实例化 bean 对象,一般是通过反射调用默认的构造器。
  2. populateBean bean 属性注入,在这个步骤会从 Spring 容器中查找对应属性字段的值,解决循环依赖问题。
  3. initializeBean 调用的 bean 定义的初始化方法。

Spring 解决循环思路是第一步创建 bean 实例后,就将这个未进行属性注入的 bean 通过 addSingletonFactory 添加到 beanFactory 的容器中,这样即使这个对象还未创建完成就可以通过 getSingleton(beanName) 直接在容器中找到这个 bean。过程如下所示:

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


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

Spring 循环引用(一)一个循环依赖引发的 BUG的更多相关文章

  1. spring 在容器中一个bean依赖另一个bean 需要通过ref方式注入进去 通过构造器 或property

    spring  在容器中一个bean依赖另一个bean 需要通过ref方式注入进去 通过构造器 或property

  2. block中防止循环引用的一个高大上的宏定义

    看惯了什么tempSelf weakSelf,来点高大的 #define weakify(...) \ rac_keywordify \ metamacro_foreach_cxt(rac_weaki ...

  3. Spring学习笔记:Spring概述,第一个IoC依赖注入案例

    一.Spring的优点 企业及系统: 1.大规模:用户数量多.数据规模大.功能众多 2.性能和安全要求高 3.业务复杂 4.灵活应变 Java技术:高入侵式依赖EJB技术框架-->Spring框 ...

  4. 一个int类型引发的bug

    一.引言 今天我在项目开发中,遭遇了一个莫名其妙的问题,概括加抽象后形成如下问题:在使用MyBatis的XML语句实现Dao层接口 List<Person> selectBySome(@P ...

  5. 关于vector变量的size,是一个无符号数引发的bug。LeetCode 3 sum

    class Solution { public: vector<vector<int>> threeSum(vector<int>& a) { vector ...

  6. Spring 循环引用(二)源码分析

    Spring 循环引用(二)源码分析 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring 循环引用相关文章: & ...

  7. 理解 ARC 下的循环引用

    本文由 伯乐在线 - nathanw 翻译,dopcn 校稿.未经许可,禁止转载!英文出处:digitalleaves.com.欢迎加入翻译组. ARC 下的循环引用类似于日本的 B 级恐怖片.当你刚 ...

  8. block使用小结、在arc中使用block、如何防止循环引用

    引言 使用block已经有一段时间了,感觉自己了解的还行,但是几天前看到CocoaChina上一个关于block的小测试主题: [小测试]你真的知道blocks在Objective-C中是怎么工作的吗 ...

  9. iOS循环引用问题

    今天面试问道了循环引用,所以就看了看,原来只是知道使用了Block容易造成循环引用.今天就来简单的介绍一些循环引用. 先来简单介绍一下什么是循环引用? 循环引用可以简单的理解成:A对象引用了B对象,B ...

随机推荐

  1. artTemplate js模板引擎动态给html赋值

    html放到$("#area").append(html);之前,否则文档流获取不到#area <table width="90%" class=&quo ...

  2. javascript 页面导出功能

    javascript 页面导出功能 <a class="btn" href="javascript:void(0);" onclick="win ...

  3. 不同CSS技术及其CSS性能

    OOCSS样式:一个主class,包含所有的共同规则,然后一个独特的规则使用其他class .box {padding:25px;border:1px solid #000;border-radius ...

  4. centos 7.3+nginx+jira(.bin)+mysql

    JIRA 安装参考资料 http://www.cnblogs.com/ilanni/p/6200875.html 注意服务启动与关闭 service jira stop service jira st ...

  5. 第二章 向量(d5)有序向量:插值查找

  6. Linux系统清除缓存

    1)缓存机制介绍在Linux系统中,为了提高文件系统性能,内核利用一部分物理内存分配出缓冲区,用于缓存系统操作和数据文件,当内核收到读写的请求时,内核先去缓存区找是否有请求的数据,有就直接返回,如果没 ...

  7. CentOS ./configure && make && make install详解

    码的安装一般由3个步骤组成:配置(configure).编译(make).安装(make install). 在Linux中利用源码包安装软件最重要的就是要仔细阅读安装包当中的README  INST ...

  8. 23【notepad++】修改背景颜色

    notepad++是一款功能丰富的编辑器,运行在windows平台上的编辑工具. 但它默认设置是白色背景,黑色文字,长时间看很刺眼.那么怎么设定成为暗色背景,亮色文字呢? 点击,设置->语言格式 ...

  9. eclipse及tomcat设置编码

    新装的eclipse新导入项目会乱码,解决办法: 右击项目选properties,找到resources选择utf-8 改后乱码解决 乱码解决后可能还会有红叉,project clean即可 一劳永逸 ...

  10. 编程,将data段中的字符串转化成大写

    assume cs:code data segment db 'conversation' data ends code segment start: mov ax,data mov ds,ax ca ...