Spring IoC 循环依赖的处理
前言
本系列全部基于 Spring 5.2.2.BUILD-SNAPSHOT 版本。因为 Spring 整个体系太过于庞大,所以只会进行关键部分的源码解析。
本篇文章主要介绍 Spring IoC 是怎么解决循环依赖的问题的。
正文
什么是循环依赖
循环依赖就是循环引用,就是两个或多个 bean 相互之间的持有对方,比如A引用B,B引用A,像下面伪代码所示:
public class A {
private B b;
// 省略get和set方法...
}
public class B {
private A a;
// 省略get和set方法...
}
Spring 如何解决循环依赖
Spring IoC 容器对循环依赖的处理有三种情况:
- 构造器循环依赖:此依赖 Spring 无法处理,直接抛出
BeanCurrentlylnCreationException异常。 - 单例作用域下的
setter循环依赖:此依赖 Spring 通过三级缓存来解决。 - 非单例的循环依赖:此依赖 Spring 无法处理,直接抛出
BeanCurrentlylnCreationException异常。
构造器循环依赖
还是假设上面的A和B类是构造器循环依赖,如下所示:
public class A {
private B b;
public A(B b) {
this.b = b;
}
// 省略get和set方法...
}
public class B {
private A a;
public B(A a) {
this.a = a;
}
// 省略get和set方法...
}
然后我们在 XML 中配置了构造器自动注入,如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="a" class="com.leisurexi.ioc.circular.reference.A" autowire="constructor" />
<bean id="b" class="com.leisurexi.ioc.circular.reference.B" autowire="constructor" />
</beans>
那么我们在获取 A 时,首先会进入 doGetBean() 方法(该方法在Spring IoC bean 的加载中分析过),会进行到如下代码块:
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
// 省略其它代码...
// 如果 bean 的作用域是单例
if (mbd.isSingleton()) {
// 创建和注册单例 bean
sharedInstance = getSingleton(beanName, () -> {
try {
// 创建 bean 实例
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
// 获取bean实例
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// 省略其它代码...
}
上面方法中的 getSingleton() 方法会判断是否是第一次创建该 bean,如果是第一次会先去创建 bean,也就是调用 ObjectFacoty 的 getObject() 方法,即调用 createBean() 方法创建 bean 前,会先将当前正要创建的 bean 记录在缓存 singletonsCurrentlyInCreation 中。
在创建A时发现依赖 B,便先去创建 B;B在创建时发现依赖A,此时A因为是通过构造函数创建,所以没创建完,便又去创建A,发现A存在于 singletonsCurrentlyInCreation,即正在创建中,便抛出 BeanCurrentlylnCreationException 异常。
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
// 加锁
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
// 一级缓存中不存在当前 bean,也就是当前 bean 第一次创建
if (singletonObject == null) {
// 如果当前正在销毁 singletons,抛出异常
if (this.singletonsCurrentlyInDestruction) {
throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)");
}
// 创建单例 bean 之前的回调
beforeSingletonCreation(beanName);
boolean newSingleton = false;
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<>();
}
try {
// 获取 bean 实例
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
// 省略异常处理...
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
// 创建单例 bean 之后的回调
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 将 singletonObject 放入一级缓存,并从二级和三级缓存中移除
addSingleton(beanName, singletonObject);
}
}
// 返回 bean 实例
return singletonObject;
}
}
// 单例 bean 创建前的回调方法,默认实现是将 beanName 加入到当前正在创建 bean 的缓存中,
// 这样便可以对循环依赖进行检测
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
// 单例 bean 创建后的回调方法,默认实现是将 beanName 从当前正在创建 bean 的缓存中移除
protected void afterSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
}
}
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// 这边bean已经初始化完成了,放入一级缓存
this.singletonObjects.put(beanName, singletonObject);
// 移除三级缓存
this.singletonFactories.remove(beanName);
// 移除二级缓存
this.earlySingletonObjects.remove(beanName);
// 将 beanName 添加到已注册 bean 缓存中
this.registeredSingletons.add(beanName);
}
}
setter循环依赖
还是假设上面的A和B类是 field 属性依赖注入循环依赖,如下所示:
public class A {
private B b;
// 省略get和set方法...
}
public class B {
private A a;
// 省略get和set方法...
}
然后我们在 XML 中配置了按照类型自动注入,如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="a" class="com.leisurexi.ioc.circular.reference.A" autowire="byType" />
<bean id="b" class="com.leisurexi.ioc.circular.reference.B" autowire="byType" />
</beans>
Spring 在解决单例循环依赖时引入了三级缓存,如下所示:
// 一级缓存,存储已经初始化完成的bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存,存储已经实例化完成的bean
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存,存储创建bean实例的ObjectFactory
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 按先后顺序记录已经注册的单例bean
private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
首先在创建A时,会进入到 doCreateBean() 方法(前面的流程可以查看Spring IoC bean 的创建一文),如下:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
// 获取bean的实例
BeanWrapper instanceWrapper = null;
if (instanceWrapper == null) {
// 通过构造函数反射创建bean的实例,但是属性并未赋值
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
// 获取bean的实例
final Object bean = instanceWrapper.getWrappedInstance();
// 省略其它代码...
// bean的作用域是单例 && 允许循环引用 && 当前bean正在创建中
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
// 如果允许bean提前曝光
if (earlySingletonExposure) {
// 将beanName和ObjectFactory形成的key-value对放入singletonFactories缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// 省略其它代码...
}
在调用 addSingletonFactory() 方法前A的实例已经创建出来了,只是还未进行属性赋值和初始化阶段,接下来将它放入了三级缓存中,如下:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
// 加锁
synchronized (this.singletonObjects) {
// 如果一级缓存中不包含当前bean
if (!this.singletonObjects.containsKey(beanName)) {
// 将ObjectFactory放入三级缓存
this.singletonFactories.put(beanName, singletonFactory);
// 从二级缓存中移除
this.earlySingletonObjects.remove(beanName);
// 将beanName加入到已经注册过的单例bean缓存中
this.registeredSingletons.add(beanName);
}
}
}
接下来A进行属性赋值阶段(会在后续文章中单独分析这个阶段),发现依赖B,便去获取B,发现B还没有被创建,所以走创建流程;在B进入属性赋值阶段时发现依赖A,就去调用 getBean() 方法获取A,此时会进入 getSingleton() 方法(该方法的调用流程在Spring IoC bean 的加载一文中分析过),如下:
public Object getSingleton(String beanName) {
// allowEarlyReference设置为true表示允许早期依赖
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先从一级缓存中,检查单例缓存是否存在
Object singletonObject = this.singletonObjects.get(beanName);
// 如果为空,并且当前bean正在创建中,锁定全局变量进行处理
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 从二级缓存中获取
singletonObject = this.earlySingletonObjects.get(beanName);
// 二级缓存为空 && bean允许提前曝光
if (singletonObject == null && allowEarlyReference) {
// 从三级缓存中获取bean对应的ObjectFactory
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 调用预先设定的getObject(),获取bean实例
singletonObject = singletonFactory.getObject();
// 放入到二级缓存中,并从三级缓存中删除
// 这时bean已经实例化完但还未初始化完
// 在该bean未初始化完时如果有别的bean引用该bean,可以直接从二级缓存中取出返回
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
尝试一级缓存 singletonObjects (肯定没有,因为A还没初始化完全),尝试二级缓存 earlySingletonObjects(也没有),尝试三级缓存 singletonFactories,由于A通过 ObjectFactory 将自己提前曝光了,所以B能够通过 ObjectFactory.getObject() 拿到A对象(虽然A还没有初始化完全,但是总比没有好呀)。B拿到A后顺利创建并初始化完成,调用上面分析过的 addSingleton() 方法将自己放入一级缓存中。此时返回A中,A也能顺利拿到完全初始化的B进行后续的阶段,最后也将自己放入一级缓存中,并从二级和三级缓存中移除。
过程图如下所示:

非单例循环依赖
对于非单例的 bean,Spring 容器无法完成依赖注入,因为 Spring 容器不进行缓存,因此无法提前暴露一个创建中的 bean。
总结
本文主要介绍了 Spring 对三种循环依赖的处理,其实还有一种字段循环依赖,比如 @Autowired 注解标注的字段,但它和 setter 循环依赖的解决方法一样,这里就没有多说。
最后,我模仿 Spring 写了一个精简版,代码会持续更新。地址:https://github.com/leisurexi/tiny-spring。
参考
- 《Spring 源码深度解析》—— 郝佳
- https://juejin.im/post/5c98a7b4f265da60ee12e9b2
Spring IoC 循环依赖的处理的更多相关文章
- Spring IoC - 循环依赖
Spring 复习 3.循环依赖 3.1 定义 循环依赖指多个对象的创建过程中均需要注入对方对象,如下所示 class A{ B b; public A(){ } public A(B b){ thi ...
- 曹工说Spring Boot源码(29)-- Spring 解决循环依赖为什么使用三级缓存,而不是二级缓存
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- Spring的循环依赖问题
spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器如何解决循环依赖呢?首先让我们来定义循环引用类: 在Spring中将循环依赖的处理分成了3种情况: 构造器循环依赖 ...
- Spring之循环依赖
转:http://my.oschina.net/tryUcatchUfinallyU/blog/287936 概述 如何检测循环依赖 循环依赖如何解决 Spring如何解决循环依赖 主要的几个缓存 主 ...
- 再谈spring的循环依赖是怎么造成的?
老生常谈,循环依赖!顾名思义嘛,就是你依赖我,我依赖你,然后就造成了循环依赖了!由于A中注入B,B中注入A导致的吗? 看起来没毛病,然而,却没有说清楚问题!甚至会让你觉得你是不清楚spring的循环依 ...
- Spring解决循环依赖
1.Spring解决循环依赖 什么是循环依赖:比如A引用B,B引用C,C引用A,它们最终形成一个依赖环. 循环依赖有两种 1.构造器循环依赖 构造器注入导致的循环依赖,Spring是无法解决的,只能抛 ...
- Spring当中循环依赖很少有人讲,今天一起来学习!
网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...
- Spring的循环依赖,学就完事了【附源码】
目录 啥是循环依赖? Spring可以解决循环依赖的条件 Spring如何去解决循环依赖 SpringBean的创建流程 Spring维护的三级缓存 getSingleton getSingleton ...
- Spring的循环依赖
本文简要介绍了循环依赖以及Spring解决循环依赖的过程 一.定义 循环依赖是指对象之间的循环依赖,即2个或以上的对象互相持有对方,最终形成闭环.这里的对象特指单例对象. 二.表现形式 对象之间的循环 ...
随机推荐
- 【JAVA习题十五】两个乒乓球队进行比赛,各出三人。甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比赛名单。有人向队员打听比赛的名单。a说他不和x比,c说他不和x,z比,请编程序找出三队赛手的名单。
package erase; public class 选人比赛 { public static void main(String[] args) { // TODO Auto-generated m ...
- 当微信小程序遇到云开发,再加上一个类似 ColorUI 的模板,人人都能做小程序了
作为一个 Java 程序员,早就想尝试一把微信小程序,但是一直苦于没有想法,再加上做一个漂亮的页面实在不太擅长. 由于自己比较喜欢历史,经常看历史方面的书.在一次梳理中国现有的朝代时,突然想到,要是可 ...
- CVE-2020-0796永恒之黑复现POC EXP以及修复方案
描述: 北京时间3月12日,针对最新披露的SMB远程代码执行漏洞(CVE-2020-0796),微软官方发布了针对Windows 10/Server禁用SMBv3(SMB 3.1.1版本)协议压缩的安 ...
- Java实现 蓝桥杯VIP 算法训练 与1连通的点的个数(并查集)
试题 算法训练 与1连通的点的个数 资源限制 时间限制:1.0s 内存限制:256.0MB 问题描述 没有问题描述. 输入格式 输入的第一行包含两个整数n, m n代表图中的点的个数,m代表边的个数 ...
- Java实现 LeetCode 729 我的日程安排表 I(二叉树)
729. 我的日程安排表 I 实现一个 MyCalendar 类来存放你的日程安排.如果要添加的时间内没有其他安排,则可以存储这个新的日程安排. MyCalendar 有一个 book(int sta ...
- Java实现 LeetCode 415 字符串相加
415. 字符串相加 给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和. 注意: num1 和num2 的长度都小于 5100. num1 和num2 都只包含数字 0-9. num ...
- PAT A除以B
本题要求计算A/B,其中A 是不超过 1000 位的正整数,B 是 1 位正整数.你需要输出商数Q 和余数R,使得 A=B*Q+R 成立. 输入格式: 输入在一行中依次给出A 和B,中间以 1 空格分 ...
- 用斗地主的实例学会使用java Collections工具类
目录 一.背景 二.概念 1.定义 2.方法 2.1.排序方法 2.2.查找/替换方法 三.斗地主实例 3.1.代码结构 3.2.常量定义 3.3.单只牌类 3.4.玩家类 3.5.主程序 四.深入理 ...
- SaaS权限设计总结
2年前转到SaaS部门之后期间断断续续做着权限相关的业务,这篇文章主要回顾下过往的设计以及其原因和利弊. 不过因为是线上业务,会省略掉很多细节以及账号体系和权益相关得部分,只讨论权限相关. 本文也不会 ...
- .NET Core 工作单元unitofwork 实现,基于NPOCO
现有项目中的orm 并非efcore,而是非主流的npoco,本身没有自带工作单元所以需要自己手撸一个,现记录一下,基于其他orm的工作单元照例实现应该没有什么问题 该实现基于NPOCO,针对其他的O ...