Spring IoC 循环依赖源码解析:三级缓存与“提前暴露“机制
2026/9/20 19:51:02 网站建设 项目流程

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 注入场景下的循环依赖问题。你会看到从getBeandoCreateBeanpopulateBean的完整调用链,理解"提前暴露半成品 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依赖BB依赖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>

配置的核心语义:

  • Ab属性通过<property name="b" ref="b"/>引用名为b的 Bean;
  • Ba属性通过<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);

三者的职责与流转关系可以这样概括:

缓存数据结构存放内容作用
一级singletonObjectsConcurrentHashMap完整单例 Bean最终对外提供实例的"正式缓存"
二级earlySingletonObjectsConcurrentHashMap半成品 Bean(提前暴露)暂存已从三级缓存中取出的"未初始化实例"
三级singletonFactoriesHashMapObjectFactory(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最终会走到createBeandoCreateBeandoCreateBeanAbstractAutowireCapableBeanFactory中创建 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; }

关键点有两处:

  1. 时机addSingletonFactorypopulateBean(属性填充)之前执行。也就是说,Bean 一实例化完就立刻把"取提前引用"的工厂函数放进三级缓存,为可能出现的循环依赖提前铺路。
  2. 条件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 的例子,整个取用过程是:

  1. 创建A:实例化 A,把() -> getEarlyBeanReference("a", ...)放入三级缓存;
  2. 填充 A 的b属性 →getBean("b")→ 创建B:实例化 B,把() -> getEarlyBeanReference("b", ...)放入三级缓存;
  3. 填充 B 的a属性 →getBean("a")getSingleton("a")
    • 一级缓存没有a
    • a正在创建中;
    • 二级缓存没有a
    • 三级缓存中找到a的工厂函数并执行,返回未初始化的 A 半成品
    • 该半成品放入二级缓存,三级缓存中的工厂函数被移除;
  4. 把拿到的 A 半成品注入 B 的a属性,B 继续完成初始化,B 变成完整对象,注册进一级缓存;
  5. 回到第 2 步,把完整的 B 注入 A 的b属性,A 完成初始化,注册进一级缓存。

最终经历这一系列流转,半成品对象拿到了它所依赖的完整对象,完整对象又被注入回半成品对象,两个对象都成为"实例化 + 初始化"的完整单例,循环依赖被成功解开。

完整解决历程

下图为循环依赖在本工程中的完整解决历程(该图仅代表当前项目工程):

可以把整条链路浓缩为四个阶段:

  1. 实例化createBeanInstance创建裸对象;
  2. 提前暴露addSingletonFactorygetEarlyBeanReference的 lambda 写入三级缓存(属性填充之前);
  3. 递归触发populateBeanapplyPropertyValuesresolveValueIfNecessaryresolveReferencegetBean,递归创建被引用 Bean;
  4. 缓存取用:被引用 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:doCreateBeancreateBeanInstanceapplyPropertyValuesaddSingletonFactorygetEarlyBeanReference的完整源码上下文;
  • Spring-DefaultSingletonBeanRegistry.md:三级缓存字段定义、registerSingleton/addSingleton/getSingleton的完整实现与官方测试用例DefaultSingletonBeanRegistryTests
  • 依赖注入(DI).md.md):从getBean/doGetBeanpopulateBean的完整依赖注入触发链,以及BeanDefinitionValueResolver#resolveReference的递归逻辑;
  • 循环依赖.md:本文所依据的原始文档。

【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询