本文结合《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图形用户界面之列表框

    列表框通过Swing组件JList产生,其总是在屏幕上占据固定行数的空间.如果要得到列表框中被选中的元素,只需调用getSelectedValuesList(),它可以产生一个字符串数组,内容为被选中 ...

  2. Oracle数据库拼接字符串

    Oracle数据库中字符串拼接的实现 select count() from THT_HTFKJL where ht=1: 假如结果为:31.例如上面例子想要给结果拼接字符串有二种实现方法,第一种使用 ...

  3. 前台提交数据(表单数据、Json数据及上传文件)的类型

    MIME (Multipurpose Internet Mail Extensions) 是描述内容类型的互联网标准.Clients use this content type or media ty ...

  4. opencv图像处理基础 (《OpenCV编程入门--毛星云》学习笔记一---五章)

    #include <QCoreApplication> #include <opencv2/core/core.hpp> #include <opencv2/highgu ...

  5. 根据自定义区域裁剪ArcGIS切片地图服务

    切片地图服务是访问地图最快捷的服务方式.假如要根据地理区域对切图进行访问控制,往往只能针对不同地理区域制作相应的地图,并发布为切片地图服务.而一般在切图的时候又是按全区域实施的,所以给切片管理者造成不 ...

  6. MyBatis -01- 初识 MyBatis + MyBatis 环境搭建

    MyBatis -01- 初识 MyBatis + MyBatis 环境搭建 MyBatis 本是 apache 的一个开源项目 iBatis(iBATIS = "internet" ...

  7. linux 自启动 ,让生活更美好!!

    systemctl enable svnserve.service systemctl enable iptables.service systemctl enable firewalld.servi ...

  8. Xcode 5.1 编译模拟器以及真机都能使用的静态库

    Xcode 5.1.dmg 下载地址 http://pan.baidu.com/s/1jGJpKm6 1.新建 Framework & Library 工程 我起名叫ShowInfo,下面为其 ...

  9. [C++] 用Xcode来写C++程序[1] 新建C++项目工程

    用Xcode来写C++程序[1] 新建C++项目工程 第一节从新建工程并编译C++源码开始 新建工程 源码: // // main.cpp // YeHelloWorld // // Created ...

  10. marquee 标签的鼠标放上去滚动效果 鼠标离开继续滚动

    效果很实用,可以轻松的实现鼠标放上去停止滚动.鼠标离开继续滚动的效果.下面是具体的用法(特别注意onMouseOver和onMouseOut是需要同时写进去才会出现比较好的效果):onMouseOut ...