Spring IoC 循环依赖源码解析:三级缓存与"提前暴露"机制
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter
导读
本文以 Spring Framework 5.3.18 源码为基线,完整剖析 IoC 容器如何借助singletonObjects / earlySingletonObjects / singletonFactories 三级缓存解决 setter 注入场景下的循环依赖问题。你会看到从getBean到doCreateBean、populateBean的完整调用链,理解"提前暴露半成品 Bean"这一核心思想,并通过一个 A→B→A 的最小可运行工程把抽象原理落地为可复现的代码。读完本文,你既能回答"Spring 为什么能解决循环依赖"的经典面试题,也能在遇到BeanCurrentlyInCreationException时快速定位问题根因。
什么是循环依赖
循环依赖(Circular Reference)指的是:一个对象依赖的链条闭环回到它自己。
A -> B -> ... -> A
即对象 A 依赖对象 B,对象 B(直接或间接)又依赖对象 A。最简单的情况就是两个对象互相引用:
- A 的属性
b引用了 B; - B 的属性
a引用了 A。
注意(本文的讨论前提):本文只讨论setter / 属性注入场景下的循环依赖,不涉及代理对象(AOP)问题。构造器注入的循环依赖在 Spring 中是无法解决的,这一点在文末会有说明。
解决思路:当一个对象已经实例化完毕、但还未初始化(属性还未填充)的时候,将它提前暴露给那个正在等它、且已经实例化好的依赖方,让依赖方先拿到这个"半成品",从而解开闭环;随后被依赖方完成自己的初始化,依赖方也完成自己的初始化,最终双方都是完整对象(实例化 + 初始化)。
这个"提前暴露"动作,正是通过 IoC 容器中的三级缓存完成的。
准备一个可复现的最小工程
为了后面能对着源码理解,先搭建一个最简单的循环依赖工程。我们只用两个类:A依赖B,B依赖A。多个类之间形成更长的依赖环时原理完全相同。
A 类:
package cn.demo1; import lombok.Getter; import lombok.Setter; @Setter @Getter public class A { private B b; }B 类:
package cn.demo1; import lombok.Getter; import lombok.Setter; @Setter @Getter public class B { private A a; }配置文件 test1.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="cn.demo1.A"> <property name="b" ref="b"/> </bean> <bean id="b" class="cn.demo1.B"> <property name="a" ref="a"/> </bean> </beans>配置的核心语义:
A的b属性通过<property name="b" ref="b"/>引用名为b的 Bean;B的a属性通过<property name="a" ref="a"/>引用名为a的 Bean;- 两个 Bean 都没有配置
lazy-init,因此 IoC 容器初始化时会自动触发依赖注入(最终也是走getBean路径,详见 依赖注入(DI) 文档.md))。
三级缓存:解决问题的物质基础
循环依赖的"提前暴露"依赖DefaultSingletonBeanRegistry类中的三个重要属性。这个类负责单例 Bean 的注册与缓存,其完整字段与测试用例可参考 Spring-DefaultSingletonBeanRegistry.md。
// 一级缓存:存放完整 Bean 对象(实例化 + 初始化),是最终对外提供的单例 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 三级缓存:存放一个 lambda 表达式(ObjectFactory),尚未真正创建半成品 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 二级缓存:存放半成品 Bean 对象(只实例化、还未初始化),即"提前暴露"的对象 private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);三者的职责与流转关系可以这样概括:
| 缓存 | 数据结构 | 存放内容 | 作用 |
|---|---|---|---|
一级singletonObjects | ConcurrentHashMap | 完整单例 Bean | 最终对外提供实例的"正式缓存" |
二级earlySingletonObjects | ConcurrentHashMap | 半成品 Bean(提前暴露) | 暂存已从三级缓存中取出的"未初始化实例" |
三级singletonFactories | HashMap | ObjectFactory(lambda) | 延迟创建提前暴露对象,只有在真正发生循环依赖时才执行 |
循环依赖问题发生在属性填充(populateBean)阶段:实例化本身没问题,问题在于给属性赋值时,发现所引用的 Bean 也正在创建、还没有最终对象可用。
在 Spring-DefaultSingletonBeanRegistry.md 的官方测试用例testSingletons中可以看到,registerSingleton通过addSingleton把完整对象放入一级缓存并同步清理二、三级缓存:
protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { this.singletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } }注意这里synchronized (this.singletonObjects)的加锁方式:对一级缓存加锁来保护三个缓存的一致性操作。
突破口:doCreateBean 与提前暴露
getBean最终会走到createBean→doCreateBean。doCreateBean是AbstractAutowireCapableBeanFactory中创建 Bean 实例的具体实现,Spring 中所有以do开头的方法都是真正干活的方法。完整的doCreateBean上下文可以对照 Spring-beanFactory.md 阅读。
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { // bean 的包装类 BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) { // 这里只是 bean 的实例化(构造对象,属性还是空的) instanceWrapper = createBeanInstance(beanName, mbd, args); } Object bean = instanceWrapper.getWrappedInstance(); Class<?> beanType = instanceWrapper.getWrappedClass(); if (beanType != NullBean.class) { mbd.resolvedTargetType = beanType; } // 一般为 true:单例 && 允许循环引用 && 当前 bean 正在创建中 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); // ....省略部分 if (earlySingletonExposure) { // 将一段 lambda 放入三级缓存:注意这是在填充属性之前完成的, // lambda 捕获的是一个还未初始化的 bean addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } Object exposedObject = bean; try { // 属性填充:循环依赖正是在这一步被触发 populateBean(beanName, mbd, instanceWrapper); // 初始化:调用 afterPropertiesSet / init-method 等 exposedObject = initializeBean(beanName, exposedObject, mbd); } // ..........省略部分 return exposedObject; }关键点有两处:
- 时机:
addSingletonFactory在populateBean(属性填充)之前执行。也就是说,Bean 一实例化完就立刻把"取提前引用"的工厂函数放进三级缓存,为可能出现的循环依赖提前铺路。 - 条件:
earlySingletonExposure需要同时满足三个条件:mbd.isSingleton():单例 Bean;this.allowCircularReferences:容器允许循环引用(默认为true,可以关闭);isSingletonCurrentlyInCreation(beanName):当前 Bean 正处于创建过程中(由beforeSingletonCreation标记)。
addSingletonFactory:把 lambda 放入三级缓存
addSingletonFactory定义在DefaultSingletonBeanRegistry中,singletonFactory参数就是我们传入的 lambda 表达式:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized (this.singletonObjects) { // 只有当一级缓存中还不存在该 beanName 时才注册 if (!this.singletonObjects.containsKey(beanName)) { // 放入三级缓存 this.singletonFactories.put(beanName, singletonFactory); // 把二级缓存中该 beanName 的半成品删除(保持状态一致) this.earlySingletonObjects.remove(beanName); // 标记当前注册的 bean this.registeredSingletons.add(beanName); } } }此时三级缓存里存的只是一个"生产半成品的工厂函数",并没有真正创建任何对象,这正是"延迟暴露"的妙处:如果没有发生循环依赖,这个 lambda 永远不会被执行,也就没有多余的开销。
lambda 所执行的方法:getEarlyBeanReference
三级缓存中的 lambda 执行时调用的是getEarlyBeanReference:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; // 普通 bean 是进不来的,只有注册了相关 BeanPostProcessor 才会进入 if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject = bp.getEarlyBeanReference(exposedObject, beanName); } } // 直接返回传进来的 bean:一个还未初始化的 bean,也就是提前暴露的对象 return exposedObject; }从源码结构可以推断:getEarlyBeanReference的主要价值在于给SmartInstantiationAwareBeanPostProcessor(典型代表是 AOP 的AbstractAutoProxyCreator)一个"提前暴露代理对象"的机会——如果该 Bean 后续会被代理,那么提前暴露给依赖方的也应当是代理对象,否则依赖方拿到的裸对象与最终代理对象不一致。这也正是 Spring-beanFactory.md 中getEarlyBeanReference章节引用了org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#getEarlyBeanReference的原因。
对于本文的普通 Bean(无 AOP)场景,getEarlyBeanReference直接返回原始的半成品对象。
属性填充:循环依赖真正爆发的时刻
实例化完成后进入populateBean(属性填充 / 依赖注入)。populateBean最终调用applyPropertyValues完成属性的解析与赋值,这个方法在 依赖注入(DI) 文档.md) 和 Spring-beanFactory.md 中均有完整源码。
protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { // 为解析后的属性值创建一份深拷贝 List<PropertyValue> deepCopy = new ArrayList<>(original.size()); boolean resolveNecessary = false; for (PropertyValue pv : original) { if (pv.isConverted()) { deepCopy.add(pv); } else { // 属性名字 String propertyName = pv.getName(); // 当你引用另一个 bean 时,它会被封装成 RuntimeBeanReference 对象,便于操作 Object originalValue = pv.getValue(); // 这里是解析属性值的工作,也就是循环依赖产生的地方 Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); // 省略.... } } }applyPropertyValues中有一个重要的方法调用resolveValueIfNecessary,它由BeanDefinitionValueResolver提供(省略无关分支):
public Object resolveValueIfNecessary(Object argName, @Nullable Object value) { // 当前 bean 的属性值类型正是 RuntimeBeanReference(对另一个 bean 的引用) if (value instanceof RuntimeBeanReference) { RuntimeBeanReference ref = (RuntimeBeanReference) value; return resolveReference(argName, ref); } // 省略... }RuntimeBeanReference是解析 XML 时对<property ref="..."/>的封装。也就是说,只要你的属性是引用另一个 Bean,最终都会走到resolveReference。
resolveReference:递归触发 getBean
resolveReference中有一段核心代码:
@Nullable private Object resolveReference(Object argName, RuntimeBeanReference ref) { // 省略... // 上面一般进不去,直接看这个重点 resolvedName = String.valueOf(doEvaluate(ref.getBeanName())); // 获取所依赖的 bean:此处可能递归调用 getBean bean = this.beanFactory.getBean(resolvedName); // 省略... }这一步是关键中的关键:在给A填充属性b时,发现b是一个RuntimeBeanReference,于是调用beanFactory.getBean("b")递归去创建B;而创建B的过程中又要填充属性a,又会调用getBean("a")。如果没有缓存兜底,这里就会无限递归,最终抛出BeanCurrentlyInCreationException。
getSingleton:从三级缓存中"救场"
getBean会进入doGetBean,其中有一段代码首先尝试从缓存中获取 Bean,获取不到才创建:
Object sharedInstance = getSingleton(beanName);获取缓存 Bean 的顺序是:先从一级缓存取,若不存在,从二级缓存取;若还是不存在,则从三级缓存取并升级到二级。
getSingleton的完整实现如下:
@Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 第一步:一级缓存中是否存在(完整对象) Object singletonObject = this.singletonObjects.get(beanName); // 第二步:一级缓存没有,且该 bean 正在创建中 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 第三步:尝试二级缓存(半成品) singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { // 第四步:再查一次二级缓存 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null) { // 第五步:从三级缓存获取 ObjectFactory(lambda) ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 第六步:执行 lambda,即 () -> getEarlyBeanReference(beanName, mbd, bean) // 因为所有普通 bean 都会提前把工厂函数放进三级缓存, // 所以这里能拿到"还未初始化的 bean",从而赋值给依赖它的对象 singletonObject = singletonFactory.getObject(); // 第七步:半成品升级到二级缓存 this.earlySingletonObjects.put(beanName, singletonObject); // 第八步:从三级缓存移除,避免重复执行 this.singletonFactories.remove(beanName); } } } } } } // 返回完整对象,或者提前暴露的未初始化对象 return singletonObject; }对照 Spring-DefaultSingletonBeanRegistry.md 中给出的getSingleton源码可以看出,本文工程版本(Spring 5.3.18)在getSingleton(beanName, allowEarlyReference)中额外做了"先从一级缓存取、再加锁二次确认"的双重检查,整体流程与文档描述一致:取用的本质就是从singletonFactories中拿出ObjectFactory并执行getObject()。
结合前面 A→B→A 的例子,整个取用过程是:
- 创建
A:实例化 A,把() -> getEarlyBeanReference("a", ...)放入三级缓存; - 填充 A 的
b属性 →getBean("b")→ 创建B:实例化 B,把() -> getEarlyBeanReference("b", ...)放入三级缓存; - 填充 B 的
a属性 →getBean("a")→getSingleton("a"):- 一级缓存没有
a; a正在创建中;- 二级缓存没有
a; - 三级缓存中找到
a的工厂函数并执行,返回未初始化的 A 半成品; - 该半成品放入二级缓存,三级缓存中的工厂函数被移除;
- 一级缓存没有
- 把拿到的 A 半成品注入 B 的
a属性,B 继续完成初始化,B 变成完整对象,注册进一级缓存; - 回到第 2 步,把完整的 B 注入 A 的
b属性,A 完成初始化,注册进一级缓存。
最终经历这一系列流转,半成品对象拿到了它所依赖的完整对象,完整对象又被注入回半成品对象,两个对象都成为"实例化 + 初始化"的完整单例,循环依赖被成功解开。
完整解决历程
下图为循环依赖在本工程中的完整解决历程(该图仅代表当前项目工程):
可以把整条链路浓缩为四个阶段:
- 实例化:
createBeanInstance创建裸对象; - 提前暴露:
addSingletonFactory把getEarlyBeanReference的 lambda 写入三级缓存(属性填充之前); - 递归触发:
populateBean→applyPropertyValues→resolveValueIfNecessary→resolveReference→getBean,递归创建被引用 Bean; - 缓存取用:被引用 Bean 在创建自己时再次触发
getBean,通过getSingleton按 一级 → 二级 → 三级 的顺序取到提前暴露的半成品,注入后双方各自完成初始化。
扩展思考:边界与限制
结合本仓库源码,还有几个值得注意的边界问题:
- 为什么需要三级缓存而不是两级?从
getEarlyBeanReference的源码可以看出,三级缓存中存放的ObjectFactory是"延迟执行"的,其真正目的是让SmartInstantiationAwareBeanPostProcessor(如 AOP 代理处理器)有机会在"循环依赖发生时"才介入生成提前暴露对象。如果只用两级缓存(直接缓存半成品),就无法在暴露前完成这类后处理,代理对象与最终对象就会不一致。这是从 Spring-beanFactory.md 中getEarlyBeanReference章节引用的AbstractAutoProxyCreator实现可以推断出的设计意图。 - 可以关闭循环依赖:
earlySingletonExposure依赖this.allowCircularReferences(默认为true)。可以通过配置将其置为false,此时发生循环依赖会直接抛出BeanCurrentlyInCreationException。 - 构造器注入无法解决循环依赖:因为构造器注入在
createBeanInstance阶段就必须拿到完整的依赖对象,此时 Bean 尚未实例化完成,三级缓存还没来得及放入任何工厂函数,因此会直接失败。本文讨论的 setter 注入之所以可行,正是因为"实例化"与"属性填充"是两个可分离的阶段。 - prototype 作用域的 Bean 无法解决循环依赖:原型 Bean 每次创建新实例、不进入单例缓存体系,同样会抛
BeanCurrentlyInCreationException。
延伸阅读
如果你希望把这条调用链的上下游补齐,本仓库提供了配套的源码级文档:
- Spring-beanFactory.md:
doCreateBean、createBeanInstance、applyPropertyValues、addSingletonFactory、getEarlyBeanReference的完整源码上下文; - Spring-DefaultSingletonBeanRegistry.md:三级缓存字段定义、
registerSingleton/addSingleton/getSingleton的完整实现与官方测试用例DefaultSingletonBeanRegistryTests; - 依赖注入(DI).md.md):从
getBean/doGetBean到populateBean的完整依赖注入触发链,以及BeanDefinitionValueResolver#resolveReference的递归逻辑; - 循环依赖.md:本文所依据的原始文档。
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考