本文结合《Spring源码深度解析》来分析Spring 5.0.6版本的源代码。若有描述错误之处,欢迎指正。

实例化bean是一个非常复杂的过程,而其中比较难以理解的就是对循环依赖的解决, 不管之前读者有没有循环依赖方面的研究,这里有必要先对此知识点稍作回顾。

一. 什么是循环依赖

循环依赖就是循环引用,就是两个或多个bean相互之间的持有对方,比如CirdeA引用 CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环。此处不是循环调用,循环调用是方法之间的环调用,如下图所示。

循环调用是无法解决的,除非有终结条件,否则就是死循环,最终导致内存溢出错误。

二. Spring如何解决循环依赖

Spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器如何解决循环依赖呢?首先让我们来定义循环引用类:

public class TestA {

    private TestB testB;

    public TestA() {
} public void a() {
testB.b();
} public TestB getTestB() {
return testB;
} public void setTestB(TestB testB) {
this.testB = testB;
}
}
public class TestB {

    private TestC testC;

    public TestB() {
} public void b() {
testC.c();
} public TestC getTestC() {
return testC;
} public void setTestC(TestC testC) {
this.testC = testC;
}
}
public class TestC {

    private TestA testA;

    public TestC() {
} public void c() {
testA.a();
} public TestA getTestA() {
return testA;
} public void setTestA(TestA testA) {
this.testA = testA;
}
}

在Spring中将循环依赖的处理分成了3种情况。

1. 构造器循环依赖

表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。

如在创建TestA类时,构造器需要TestB类,那将去创建TestB,在创建TestB类时又发现需要TestC类,则又去创建TestC,最终在创建TestC时发现又需要TestA,从而形成一个环, 没办法创建。

Spring容器将每一个正在创建的bean标识符放在一个“当前创建bean池”中,bean标识符在创建过程中将一直保持在这个池中,因此如果在创建bean过程中发现自己已经在“当前创建bean池”里时,将抛出BeanCurrentlylnCreationFexception异常表示循环依赖;而对于创建完毕的bean将从“当前创建bean池”中淸除掉。

我们通过一个直观的测试用例来进行分析。

(1)创建配置文件。

<bean id = "testA" class = "com.bean.TestA">
<constructor-arg index = "0" ref = "testB"/>
</bean>
<bean id = "testB" class = "com.bean.TestB">
<constructor-arg index="0" ref = "testC"/>
</bean>
<bean id = "testC" class = "com.bean.TestC">
<constructor-arg index="0" ref="testA"/>
</bean>

(2)创建测试用例。

@Test(expected = BeanCurrentlylnCreationException.class)
public void testCircleByConstructor() throws Throwable {
try {
new ClassPathXmlApplicationContext("test-xml");
} catch(Exception e) {
// 因为要在创建testC时抛出
Throwable el = e.getCause().getCause().getCause{);
throw el;
}
}

针对以上代码的分析如下:

  • Spring容器创建“testA” bean,首先去“当前创建bean池”查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数“testB”,并将“testA”标识符放到“当前创建bean池”。
  • Spring容器创建“testB” bean,首先去“当前创建bean池”查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数“testC”,并将“testB”标识符放到“当前创建bean池。
  • Spring容器创建“testC” bean,首先去“当前创建bean池”查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数“testA”,并将“testC”标识符放到“当前创建Bean池”。
  • 到此为止Spring容器要去创建“testA” bean,发现该bean标识符在“当前创建bean 池”中,因为表示循环依赖,抛出BeanCurrentlylnCreationException。

2. setter循环依赖

表示通过setter注入方式构成的循环依赖。对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注人但未完成其他步骤(如setter注人)的bean来完成的,而且只能解决单例作用域的bean循环依赖。通过提前暴露一个单例工厂方法,从而使其他bean能引用到该bean,如下代码所示:

addSingletonFactory(beanName, new ObjectFactory() {
public Object getObject() throws BeansCxception {
return getEarlyBeanReference(beanName, mbdr bean);
});

具体步骤如下:

(1)Spring容器创建单例“testA” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testA”标识符放到“当前创建bean池”,然后进行setter注人“testB”。

(2)Spring容器创建单例“testB” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testB”标识符放到“当前创建bean池”,然后进行setter注人“testC”。

(3)Spring容器创建单例“testC” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testC”标识符放到“当前创建bean池',然后进行setter注人“testA”。进行注人“testA”时由于提前暴露了 “ObjectFactory” 工厂,从而使用它返回提前暴露的创建中的bean。

(4)最后在依赖注入“testB”和“testA”,完成setter注人。

3. prototype范围的依赖处理

对于“prototype”作用域bean,Spring容器无法完成依赖注人,因为Spring容器不进行缓存“prototype”作用域的bean,因此无法提前暴露一个创建中的bean。示例如下:

(1)创建配置文件。

<bean id = "testA" class = "com.bean.CircleA" scope = "prototype">
<property name = "testB" ref = "testB"/>
</bean>
<bean id = "testB" class = "com.bean.CircleB" scope = "prototype">
<property name = "testC" ref = "testC"/>
</bean>
<bean id = "testC" class = "com.bean.CircleC" scope="prototype">
<property name = "testA" ref = "testA"/>
</bean>

(2)创建测试用例。

@Test(expected = BeanCurrentlylnCreationException.class)
public void testCircleBySetterAndPrototype() throws Throwable {
try {
ApplicationContext context = new ClassPathXmlApplicationContext("test-prototype.xml");
System.out.println(context.getBean("testA"));
} catch(Exception e) {
Throwable el = e.getCause().getCause().getCause{);
throw el;
}
}

对于 “singleton” 作用域 bean,可以通过 “setAllowCircularReferences(false); ”来禁用循环引用。

Spring源码分析(十七)循环依赖的更多相关文章

  1. Spring源码分析之循环依赖及解决方案

    Spring源码分析之循环依赖及解决方案 往期文章: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostPro ...

  2. Spring源码-IOC部分-循环依赖-用实例证明去掉二级缓存会出现什么问题【7】

    实验环境:spring-framework-5.0.2.jdk8.gradle4.3.1 Spring源码-IOC部分-容器简介[1] Spring源码-IOC部分-容器初始化过程[2] Spring ...

  3. Spring 源码分析之 bean 依赖注入原理(注入属性)

         最近在研究Spring bean 生命周期相关知识点以及源码,所以打算写一篇 Spring bean生命周期相关的文章,但是整理过程中发现涉及的点太多而且又很复杂,很难在一篇文章中把Spri ...

  4. Spring源码分析之Bean的创建过程详解

    前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...

  5. Spring源码分析——BeanFactory体系之抽象类、类分析(二)

    上一篇分析了BeanFactory体系的2个类,SimpleAliasRegistry和DefaultSingletonBeanRegistry——Spring源码分析——BeanFactory体系之 ...

  6. Spring源码分析——BeanFactory体系之抽象类、类分析(一)

    上一篇介绍了BeanFactory体系的所有接口——Spring源码分析——BeanFactory体系之接口详细分析,本篇就接着介绍BeanFactory体系的抽象类和接口. 一.BeanFactor ...

  7. 【spring源码分析】IOC容器初始化(总结)

    前言:在经过前面十二篇文章的分析,对bean的加载流程大致梳理清楚了.因为内容过多,因此需要进行一个小总结. 经过前面十二篇文章的漫长分析,终于将xml配置文件中的bean,转换成我们实际所需要的真正 ...

  8. 【spring源码分析】IOC容器初始化(七)

    前言:在[spring源码分析]IOC容器初始化(六)中分析了从单例缓存中加载bean对象,由于篇幅原因其核心函数 FactoryBeanRegistrySupport#getObjectFromFa ...

  9. spring源码分析系列

    spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor spring源码分析系列 ...

  10. Spring源码分析(十八)创建bean

    本文结合<Spring源码深度解析>来分析Spring 5.0.6版本的源代码.若有描述错误之处,欢迎指正. 目录 一.创建bean的实例 1. autowireConstructor 2 ...

随机推荐

  1. java图形界面之图形化按钮

    要将按钮图形化,只需创建一个ImageIcon对象,将图形路径赋予ImageIcon对象,然后将该对象传递给按钮即可. 此处涉及eclipse中图形的路径设置,包括(项目路径下.非项目路径下.相对路径 ...

  2. c#如何禁用win7的任务管理器

    以前制作的桌面锁屏软件虽然也禁用过任务管理器,但是采取的是比较笨的方法,而且对操作系统还有一定的危害.因为任务管理也是一个窗体也就是说它中也是一个独立进程,所以只需要强制性关闭这个进程即可以关闭任务管 ...

  3. github上手实践教程

    简介: SSH公私钥的使用 github的使用 git 工具的基本使用 基本步骤: 一.github的使用 1.github账号的创建[官网一步一步创建就行了,这一步骤省略] 2.创建远程仓库: 创建 ...

  4. <Android 基础(二十八)> Fragment (1)

    简介 Fragment,碎片,常用的内容,但是一直没有系统的学习下它的使用方法,花几天抽空看看随便记录一下. 生命周期 来自官网的图片一目了然. 自测试结果: 基本使用 1.自定义一个Fragment ...

  5. ZZCMS8.2 用户密码重置漏洞

    前言 一个找回密码处的逻辑漏洞, 还是有点意思的. 正文 首先是定位找回密码功能对应的代码位置,使用找回密码的功能,然后抓包即可 下面去 getpassword.php 里面看看, 首先包含了一些文件 ...

  6. idapython 开发

    调试方法 使用 pydevd 然后在需要调试处加入调试代码 GetOperandValue 作用 参数1: ea 虚拟地址 参数2: 操作数号 返回指令的操作数的被解析过的值 文档 def GetOp ...

  7. Pig parallel reduce并行执行数

    parallel语句可以附加到Pig Latin中任一个关系操作符后面,然后它会控制reduce阶段的并行,因此只有对与可以触发reduce过程的操作符才有意义.     可以触发reduce过程的操 ...

  8. 分页函数 pager.py

    #!/usr/bin/python env # coding:utf-8 class Pagination(object): # 数据总条数 当前页 每页显示条数 最多显示页面 def __init_ ...

  9. git bash 出现vim弹框的时候怎么退出

    如果是输出状态,首先按Esc键退出输入状态,然后按Shift+“;”,再输入q!或wq!(不保存改动,wq!是保存文件的写入修改)退出. so easy!!

  10. TFS工具(tf.exe)使用与强制解除锁定签出

    在工作区 NTP01_SUNTAI 中,NTP01TFSUser 为 签出 锁定了项 $/NTP01/EIPD/EIPD.Client/Views/Courseware/EditorOne.xaml. ...