news 2026/9/13 9:28:02

Spring IoC 循环依赖源码解析:三级缓存如何提前暴露半成品 Bean(source-code-hunter)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring IoC 循环依赖源码解析:三级缓存如何提前暴露半成品 Bean(source-code-hunter)

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);
缓存容器类型存放内容作用
一级缓存singletonObjectsConcurrentHashMap(256)完整 Bean(实例化 + 初始化完成)最终对外提供单例的入口
二级缓存earlySingletonObjectsConcurrentHashMap(16)提前暴露的半成品 Bean(仅实例化)缓存已通过三级缓存工厂产出的引用,避免重复执行工厂
三级缓存singletonFactoriesHashMap(16)ObjectFactorylambda 表达式延迟到“真正发生循环依赖”时才生成提前引用

此外还有一个容易被忽略的状态集合singletonsCurrentlyInCreationCollections.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的三个条件:

  1. mbd.isSingleton()—— 只有单例 Bean 才需要缓存与复用;
  2. this.allowCircularReferences—— 容器级开关,默认为true,可关闭;
  3. 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为锁对象,保证与后续getSingletonaddSingleton的原子性。

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 为例,将上述源码串成完整时序:

  1. getBean("a")doCreateBean(a):实例化 A 得到半成品a0
  2. earlySingletonExposure为真,把() -> getEarlyBeanReference("a", mbd, a0)放入三级缓存,并标记 A 正在创建;
  3. populateBean(a)解析<property name="b" ref="b"/>resolveReferencegetBean("b")
  4. doCreateBean(b):实例化 B 得到半成品b0,B 的三级缓存就位,标记 B 正在创建;
  5. populateBean(b)解析<property name="a" ref="a"/>resolveReferencegetBean("a")
  6. getSingleton("a"):一级缓存无 A → A 正在创建 → 二级缓存无 → 从三级缓存取出工厂并执行 lambda,得到半成品a0,同时把a0放入二级缓存、移除三级缓存条目;
  7. B 拿到半成品a0,完成属性填充与初始化,B 成为完整对象;随后addSingleton("b")将完整 B 放入一级缓存;
  8. 回到步骤 3 的resolveReference,把完整 B 注入 A 的b属性;A 继续完成initializeBean,成为完整对象并进入一级缓存。

最终效果:B 拿到的是 A 的半成品引用,A 拿到的是 B 的完整对象,双方都正确完成创建。对应示意图即本文开头的 images/spring/循环依赖.png。

机制的适用边界与源码依据

  • 只对单例 Bean 生效earlySingletonExposure的第一个条件是mbd.isSingleton(),且addSingletonFactory只服务于单例。从代码结构可以直接推断:prototype(多例)Bean 每次都会新建对象、不进入单例缓存,无法享受提前暴露机制。
  • 只对 setter / 字段注入生效:提前暴露发生在createBeanInstance之后,此时构造器已经执行完毕,因此构造器注入导致的循环依赖无法被该机制兜底(会在构造阶段就递归卡死)。
  • 可通过开关关闭:容器属性allowCircularReferences置为falseearlySingletonExposure恒为假,此时再出现循环依赖会直接抛出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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 9:23:41

告别臃肿Postman:用开源轻量工具Bruno实现接口调试与自动化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 9:23:34

IFC转Revit模型瘦身实战:解决螺栓冗余与材质膨胀

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 9:23:27

51单片机步进电机控制与LCD1602显示系统设计

简介&#xff1a;基于51单片机的步进电机控制系统是一套面向电子设计竞赛与单片机初学者的完整参考资料&#xff0c;覆盖步进电机步数设定、方向切换、通电拍数选择&#xff08;4拍/8拍&#xff09;以及LCD1602实时显示等核心需求。资料同时提供原理图、流程图、物料清单、仿真…

作者头像 李华
网站建设 2026/9/13 9:23:05

SpringBoot+Vue构建露营装备租赁系统实践

1. 项目背景与核心价值户外露营活动近年来在国内呈现爆发式增长&#xff0c;据行业数据显示&#xff0c;2023年参与露营活动的人数较前一年增长了近200%。这种快速增长带来了对露营装备租赁服务的强烈需求&#xff0c;但传统线下租赁模式存在诸多痛点&#xff1a;装备信息不透明…

作者头像 李华
网站建设 2026/9/13 9:22:22

Coze 2.0技术架构解析:从意图理解到动态执行

1. Coze 2.0技术架构演进解析Coze 2.0的技术架构实现了从"被动响应"到"主动协作"的范式转变。这个转变背后是三个核心组件的协同工作&#xff1a;意图理解引擎&#xff1a;采用多模态输入处理技术&#xff0c;能够解析用户自然语言中的显性和隐性需求。与1…

作者头像 李华