Spring源码分析(十七)循环依赖
本文结合《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源码分析(十七)循环依赖的更多相关文章
- Spring源码分析之循环依赖及解决方案
Spring源码分析之循环依赖及解决方案 往期文章: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostPro ...
- Spring源码-IOC部分-循环依赖-用实例证明去掉二级缓存会出现什么问题【7】
实验环境:spring-framework-5.0.2.jdk8.gradle4.3.1 Spring源码-IOC部分-容器简介[1] Spring源码-IOC部分-容器初始化过程[2] Spring ...
- Spring 源码分析之 bean 依赖注入原理(注入属性)
最近在研究Spring bean 生命周期相关知识点以及源码,所以打算写一篇 Spring bean生命周期相关的文章,但是整理过程中发现涉及的点太多而且又很复杂,很难在一篇文章中把Spri ...
- Spring源码分析之Bean的创建过程详解
前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...
- Spring源码分析——BeanFactory体系之抽象类、类分析(二)
上一篇分析了BeanFactory体系的2个类,SimpleAliasRegistry和DefaultSingletonBeanRegistry——Spring源码分析——BeanFactory体系之 ...
- Spring源码分析——BeanFactory体系之抽象类、类分析(一)
上一篇介绍了BeanFactory体系的所有接口——Spring源码分析——BeanFactory体系之接口详细分析,本篇就接着介绍BeanFactory体系的抽象类和接口. 一.BeanFactor ...
- 【spring源码分析】IOC容器初始化(总结)
前言:在经过前面十二篇文章的分析,对bean的加载流程大致梳理清楚了.因为内容过多,因此需要进行一个小总结. 经过前面十二篇文章的漫长分析,终于将xml配置文件中的bean,转换成我们实际所需要的真正 ...
- 【spring源码分析】IOC容器初始化(七)
前言:在[spring源码分析]IOC容器初始化(六)中分析了从单例缓存中加载bean对象,由于篇幅原因其核心函数 FactoryBeanRegistrySupport#getObjectFromFa ...
- spring源码分析系列
spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor spring源码分析系列 ...
- Spring源码分析(十八)创建bean
本文结合<Spring源码深度解析>来分析Spring 5.0.6版本的源代码.若有描述错误之处,欢迎指正. 目录 一.创建bean的实例 1. autowireConstructor 2 ...
随机推荐
- python学习之老男孩python全栈第九期_day016作业
1. 请利用filter()过滤出1~100中平方根是整数的数,即结果应该是: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] import math def func( ...
- js处理包含中文的字符串
场景: js中String类型自带的属性length获取的是字符串的字符数目,但是前端经常会需要限制字符串的显示长度,一个中文字符又大概占两个英文小写字符的显示位置,所以中英文混合的情况下用lengt ...
- p2p项目工具类
1.用于存放当前用户的上下文UserContext package com.xmg.p2p.base.util; import javax.servlet.http.HttpSession; impo ...
- opencv3.2.0 分离颜色通道&多通道图像混合
##名称:分离颜色通道&多通道图像混合 ##平台:QT5.7.1+OpenCV3.2.0 ##时间:2017年12月11日 /***************创建QT控制台程序********* ...
- aix系统下的websphere的静默安装
一:环境 aix5.3,websphere6(ND版本,WebSphereV6.1_for_AIX_64-bit_Support.tar),注意:aix和websphere的版本问题 二:安装 ...
- logminer系列文章一(logminer的使用)
转自 http://blog.itpub.net/26613085/viewspace-1064008/ 1.安装logminer以及生成logminer数据字典所需要的包(需使用sys用户) [or ...
- Django 请求参数
Django 请求参数 1.获取URL路径中的参数 需求:假设用户访问127.0.0.1/user/1/2,你想获取1,2.应该怎么操作呢? (1)未命名参数(位置参数) # 在项目下的urls.py ...
- Django 简单用户注册
项目名 macboy app名 t1 1.项目macboy urls.py from django.conf.urls import url, include urlpatterns = [ url( ...
- 【Java】数组使用
package aaa; public class aaa { public static void main(String args[]) { int a[]={1,2,3,4}; for(int ...
- 将虚拟网络连接到 ExpressRoute 线路
本文通过使用 Resource Manager 部署模型和 Azure 门户,帮助将虚拟网络 (VNets) 链接到 Azure ExpressRoute 线路. 虚拟网络可以在同一个订阅中,也可以属 ...