Spring IoC 循环依赖源码解析:三级缓存如何提前暴露半成品 Bean(source-code-hunter)
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
Spring 容器在创建单例 Bean 时,如果出现 A 依赖 B、B 又依赖 A 的闭环依赖,默认情况下并不会直接抛异常,而是通过DefaultSingletonBeanRegistry中的三级缓存 + “提前暴露半成品对象”机制平滑解决。本文以 Spring 5.3.18 源码为蓝本,从可运行的 XML 工程出发,逐段剖析doCreateBean → populateBean → getSingleton的完整调用链,让你在面试与日常排障中能讲清楚“为什么三级缓存能解决 setter 注入的循环依赖”,以及这套机制适用的边界。
什么是循环依赖
循环依赖(Circular Reference)指一个对象经过依赖链后闭环回到自身:
A -> B -> ... -> A本文讨论的范围是属性注入(setter / 字段注入)场景下的循环依赖,默认不涉及代理对象问题(即普通 Bean 直接提前暴露原始实例即可,关于代理对象与getEarlyBeanReference的关系后文会展开)。
其解决思路可以浓缩为一句话:
当一个对象已经实例化完毕、还未初始化的时候,将它提前暴露(注入)给它所依赖的、已经实例化好的对象,使对方拿到的是一个“半成品引用”,待对方完成完整初始化后,再把这个完整对象注入回当前对象。
翻译成源码语言就是:实例化完成后立即把该 Bean 的“获取引用工厂”放入三级缓存,属性填充阶段遇到依赖时,先从各级缓存中取“半成品”,从而打破“必须等对方完全创建好才能引用”的死锁。
简单工程复现循环依赖
多个类相互依赖的原理与两个类完全一致,这里用最小工程cn.demo1复现。
A 类(依赖 B):
package cn.demo1; import lombok.Getter; import lombok.Setter; @Setter @Getter public class A { private B b; }B 类(依赖 A):
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><property name="b" ref="b"/>在 BeanDefinition 解析阶段会被封装为RuntimeBeanReference,这正是后面属性填充时触发递归创建的关键标记(详见 Spring-beanFactory.md 与 将 bean 解析封装成 BeanDefinition)。
三级缓存:三个“特别重要的属性”
循环依赖的解法落在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry的三个 Map 上(完整属性清单见 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(256) | 完整 Bean(实例化 + 初始化完成) | 最终对外提供单例的入口 |
二级缓存earlySingletonObjects | ConcurrentHashMap(16) | 提前暴露的半成品 Bean(仅实例化) | 缓存已通过三级缓存工厂产出的引用,避免重复执行工厂 |
三级缓存singletonFactories | HashMap(16) | ObjectFactorylambda 表达式 | 延迟到“真正发生循环依赖”时才生成提前引用 |
此外还有一个容易被忽略的状态集合singletonsCurrentlyInCreation(Collections.newSetFromMap(new ConcurrentHashMap<>(16))),它记录当前正在创建中的单例名称,是getSingleton判断“要不要查二、三级缓存”的前提。
循环依赖问题出现在**属性填充(populateBean)**阶段,而不是实例化阶段。提前暴露的动作发生在实例化之后、填充属性之前。
doCreateBean:在填充属性之前埋下“三级缓存”
进入org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean:
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 的实例化(createBeanInstance 的具体分支见 Spring-beanFactory.md) instanceWrapper = createBeanInstance(beanName, mbd, args); } Object bean = instanceWrapper.getWrappedInstance(); Class<?> beanType = instanceWrapper.getWrappedClass(); if (beanType != NullBean.class) { mbd.resolvedTargetType = beanType; } // 一般为 true:单例 && 允许循环引用 && 当前正在创建 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); // ....省略部分 if (earlySingletonExposure) { // 这里是将一段 lambda 放入三级缓存中, // 可以看见 bean 填充属性之前就会将三级缓存创建好, // 它传入的是一个还未初始化的 bean addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } // ..........省略部分 return exposedObject; }关键点在earlySingletonExposure的三个条件:
mbd.isSingleton()—— 只有单例 Bean 才需要缓存与复用;this.allowCircularReferences—— 容器级开关,默认为true,可关闭;isSingletonCurrentlyInCreation(beanName)—— 该 Bean 正处于创建过程中。
其中实例化阶段createBeanInstance的完整分支(Supplier、工厂方法、构造器推断、无参构造)可对照 Spring-beanFactory.md#createbeaninstance 查看,循环依赖问题的起点就在createBeanInstance之后、populateBean之前。
addSingletonFactory:把 lambda 放入三级缓存
// 其中 singletonFactory 是一个 lambda 表达式 protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized (this.singletonObjects) { // 如果一级缓存中不存在这个叫 beanName 的 bean if (!this.singletonObjects.containsKey(beanName)) { // 放入三级缓存中 this.singletonFactories.put(beanName, singletonFactory); // 把二级缓存中叫 beanName 的半成品 bean 删除 this.earlySingletonObjects.remove(beanName); // 标记当前注册的 bean this.registeredSingletons.add(beanName); } } }该方法的代码与 Spring-beanFactory.md#addsingletonfactory 中呈现的DefaultSingletonBeanRegistry#addSingletonFactory一致:以singletonObjects为锁对象,保证与后续getSingleton、addSingleton的原子性。
lambda 真正执行的方法:getEarlyBeanReference
三级缓存中存放的不是对象本身,而是一段 lambda:() -> getEarlyBeanReference(beanName, mbd, bean)。
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; // 普通 bean 是进不来的 if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject = bp.getEarlyBeanReference(exposedObject, beanName); } } // 直接返回传进来的 bean,返回的是一个还未初始化的 bean,是提前暴露的 return exposedObject; }普通 Bean 没有SmartInstantiationAwareBeanPostProcessor,lambda 直接返回那个还未初始化的实例,这就是“提前暴露”的实体。而当容器中注册了 AOP 相关处理器(如AbstractAutoProxyCreator#getEarlyBeanReference,见 Spring-beanFactory.md#getearlybeanreference)时,三级缓存的存在还保证了 AOP 场景下提前暴露的是“提前生成的代理对象”,从而避免同一 Bean 出现“注入的是原对象、最终用的是代理”的不一致——这也解释了为什么三级缓存里存的是“工厂”而非直接存对象:把生成引用的动作延迟到真正发生循环依赖的那一刻。
属性填充:循环依赖被触发的现场
populateBean内部最终调用applyPropertyValues完成属性解析与填充(调用链见 Spring-beanFactory.md#populatebean 与 Spring-beanFactory.md#applypropertyvalues):
protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { // Create a deep copy, resolving any references for values. 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:
// 我们当前需要的就是这个分支 public Object resolveValueIfNecessary(Object argName, @Nullable Object value) { // 当前 bean 的属性值的类型正是 RuntimeBeanReference if (value instanceof RuntimeBeanReference) { RuntimeBeanReference ref = (RuntimeBeanReference) value; return resolveReference(argName, ref); } // 省略... }而resolveReference中的重点代码是:
// 这个方法会调用 getBean @Nullable private Object resolveReference(Object argName, RuntimeBeanReference ref) { // 省略... resolvedName = String.valueOf(doEvaluate(ref.getBeanName())); // 获取所依赖的 bean —— 循环依赖就在这里被递归触发 bean = this.beanFactory.getBean(resolvedName); // 省略... }于是:填充 A 时解析到RuntimeBeanReference("b")→ 递归getBean("b")→ 创建 B → 填充 B 时解析到RuntimeBeanReference("a")→ 再次递归getBean("a")。若没有缓存机制,这一递归将无限加深直至栈溢出。
getSingleton:三级缓存“逐级降级”的读取顺序
getBean最终进入doGetBean,其中关键一行是:
Object sharedInstance = getSingleton(beanName);获取缓存的顺序是:先从一级缓存取,不存在则从二级缓存取,仍不存在则从三级缓存取并“升级”到二级缓存。
@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<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 调用三级缓存中的 lambda:() -> getEarlyBeanReference(beanName, mbd, bean) // 因为所有普通 bean 会首先提前进行三级缓存, // 所以这里会获取到还未初始化的 bean, // 从而赋值给依赖当前 singletonObject 的 bean singletonObject = singletonFactory.getObject(); // 放入二级缓存中(半成品升级) this.earlySingletonObjects.put(beanName, singletonObject); // 三级缓存中移除当前 beanName 的 lambda this.singletonFactories.remove(beanName); } } } } } } // 返回完整对象或者还未初始化的对象 return singletonObject; }注意两个细节:
- 前提是
isSingletonCurrentlyInCreation(beanName):只有当该 Bean 正处于创建中(被标记在singletonsCurrentlyInCreation里)时,才会去查二、三级缓存。否则直接返回null走完整创建流程。 - 三级缓存取出后立即“升级”到二级缓存并移除三级缓存条目:保证同一个半成品对象只通过工厂生成一次。从源码结构可以推断,二级缓存的意义正在于“缓存已生成提前引用的结果”,避免后续重复执行
ObjectFactory.getObject()(尤其是 AOP 场景下重复生成代理);而三级缓存的“延迟执行”意义在于,若整个创建过程从未发生循环依赖,那个 lambda 永远不被执行,也就没有任何额外开销。
getSingleton(String beanName, boolean allowEarlyReference)的完整对照实现(含无参版本与ObjectFactory版本)见 Spring-DefaultSingletonBeanRegistry.md#getsingleton。
全流程串联:A 与 B 的“半成品交换”
以本文的 A、B 为例,将上述源码串成完整时序:
getBean("a")→doCreateBean(a):实例化 A 得到半成品a0;earlySingletonExposure为真,把() -> getEarlyBeanReference("a", mbd, a0)放入三级缓存,并标记 A 正在创建;populateBean(a)解析<property name="b" ref="b"/>→resolveReference→getBean("b");doCreateBean(b):实例化 B 得到半成品b0,B 的三级缓存就位,标记 B 正在创建;populateBean(b)解析<property name="a" ref="a"/>→resolveReference→getBean("a");getSingleton("a"):一级缓存无 A → A 正在创建 → 二级缓存无 → 从三级缓存取出工厂并执行 lambda,得到半成品a0,同时把a0放入二级缓存、移除三级缓存条目;- B 拿到半成品
a0,完成属性填充与初始化,B 成为完整对象;随后addSingleton("b")将完整 B 放入一级缓存; - 回到步骤 3 的
resolveReference,把完整 B 注入 A 的b属性;A 继续完成initializeBean,成为完整对象并进入一级缓存。
最终效果:B 拿到的是 A 的半成品引用,A 拿到的是 B 的完整对象,双方都正确完成创建。对应示意图即本文开头的 images/spring/循环依赖.png。
机制的适用边界与源码依据
- 只对单例 Bean 生效:
earlySingletonExposure的第一个条件是mbd.isSingleton(),且addSingletonFactory只服务于单例。从代码结构可以直接推断:prototype(多例)Bean 每次都会新建对象、不进入单例缓存,无法享受提前暴露机制。 - 只对 setter / 字段注入生效:提前暴露发生在
createBeanInstance之后,此时构造器已经执行完毕,因此构造器注入导致的循环依赖无法被该机制兜底(会在构造阶段就递归卡死)。 - 可通过开关关闭:容器属性
allowCircularReferences置为false时earlySingletonExposure恒为假,此时再出现循环依赖会直接抛出BeanCurrentlyInCreationException。相关声明可对照 Spring-beanFactory.md 中doCreateBean的完整实现。 - AOP 场景:循环依赖与代理对象同时出现时,三级缓存中的工厂方法会经过
SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(如AbstractAutoProxyCreator的实现,见 Spring-beanFactory.md#getearlybeanreference)提前产出代理,保证各处拿到的引用一致;纯普通 Bean 场景则直接返回原实例。这也是“三级缓存存工厂而非直接存对象”的关键设计。
延伸阅读(仓库内关联文档)
- doCreateBean / createBeanInstance 完整实现:Bean 实例化的五种方式与属性填充、初始化全流程;
- DefaultSingletonBeanRegistry 全部缓存字段与 getSingleton 实现:一级/二级/三级缓存及
singletonsCurrentlyInCreation的声明与操作; - 依赖注入(DI).md):
populateBean触发依赖注入的完整过程与earlySingletonExposure的上下文; - BeanDefinition 的资源定位过程 与 将 bean 解析封装成 BeanDefinition:理解
<property ref="...">如何一步步变成RuntimeBeanReference。
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考