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. 详解 Tomcat 的连接数与线程池

      前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). 在前面的文章 详解Tomcat配置文件server. ...

  2. Manifest File

    [Manifest File] on every build, webpack generates some webpack runtime code, which helps webpack do ...

  3. Java 读取Excel 文件内容

    在一个项目中,有一个需求,是把excel文件的内容转换为xml格式展示.在学习如何操作的过程中,首先是如何获取excel文件,其中操作的代码如下: 1.首先是导入需要的 jar, 下载地址:https ...

  4. django搭建的站点,通过localhost能访问,但是通过ip不能访问

    问题:使用ip访问不了django站点,只能用127.0.0.1访问     解决方法:启动服务时ip使用0.0.0.0   使用gunicorn启动 gunicorn -w4 -b0.0.0.0:8 ...

  5. xnconvert 图片转换工具

    xnconvert是一款简单高效的图片转换工具.xnconvert能够批量地进行图片格式转换,并具有一定的图片处理功能,可以增加水印.特效,支持放大缩小.旋转等. xnconvert功能介绍: 你可以 ...

  6. jquery mobile两个页面以及源码(登录与注册) 转

    ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ...

  7. centos 6.5 DNS服务器 搭建

    一.DNS 介绍 DNS(Domain Name System,域名系统),因特网上作为域名和IP地址相互映射的一个分布式数据库,DNS协议运行在UDP协议之上,使用端口号53(Domain), 95 ...

  8. 江西财经大学第一届程序设计竞赛 F题 解方程

    链接:https://www.nowcoder.com/acm/contest/115/F来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32768K,其他语言65536 ...

  9. unity项目开发必备插件Asset Hunter 2(资源猎人2)

    unity必备插件 Asset Hunter 2 2.4 , 工程项目过大,垃圾太多之后的清洁利器,能识别 ,移除你用不到的资源 扫码时备注或说明中留下邮箱 付款后如未回复请至https://shop ...

  10. freetype教程网址

    http://freetype.sourceforge.net/freetype2/docs/reference/ft2-system_interface.html#FT_Stream      ht ...