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. hive mysql 初始化

    原文链接:https://juejin.im/post/59c3f8f75188255be81f91d9#heading-17 Apache Hive-2.3.0 快速搭建与使用 Hive 简介 Hi ...

  2. 微信文档采用第三方方式打开选择qq

    本篇文章主要记录解决: 微信打开文档后,----选择第三方应用打开---选择自己的项目,跳转到--列表选择界面--选择好友---然后返回到最近聊天界面,其中列表选择界面onDestroy的问题. 反编 ...

  3. FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK

    [FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK] 1.FLAG_ACTIVITY_NEW_TASK 2.FLAG_ACTIVITY_CLEAR_T ...

  4. TP3.23 与Laypage 结合进行分页

    demo地址:http://tp.ytlwin.top 控制器 <?php namespace Home\Controller; use Think\Controller; class Inde ...

  5. 第四章 栈与队列(c2)栈应用:括号匹配

  6. Appium+python自动化2-启动百度app

    一.前言 上一章节环境已经搭建好了,接下来就是需要启动APP,如何启动app呢?首先要获取包名,然后获取launcherActivity.获取这两个关键东西的方法很多,这里就不一一多说,小伙伴们可以各 ...

  7. roof

    roof - 必应词典 美[ruf]英[ruːf] n.屋顶:车顶:顶部:有…顶的 v.给…盖顶:盖上屋顶 网络房顶:楼顶:屋脊 变形复数:roofs:过去分词:roofed:现在分词:roofing ...

  8. Hashed collections哈希集合

    [定义] 有index的集合 [hash的原理] term for a situation when two different objects return the same hashcode: h ...

  9. linux下查看项目端口号,杀掉对应端口号的方法

    查看端口号:netstat -anp 结束端口号:sudo iptables -A INPUT -p tcp --dport 8012 -j DROP"

  10. public void method(),void前面的泛型T是什么

    public <T>这个T是个修饰符的功能,表示是个泛型方法,就像有static修饰的方法是个静态方法一样. 注意<T> 不是返回值,此处的返回值是void ,此处的<T ...