ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring IoC 循环依赖源码解析:三级缓存与“提前暴露“机制

Spring IoC 循环依赖源码解析:三级缓存与“提前暴露“机制 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引用了 BB 的属性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 version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd bean ida classcn.demo1.A property nameb refb/ /bean bean idb classcn.demo1.B property namea refa/ /bean /beans配置的核心语义A的b属性通过property nameb refb/引用名为b的 BeanB的a属性通过property namea refa/引用名为a的 Bean两个 Bean 都没有配置lazy-init因此 IoC 容器初始化时会自动触发依赖注入最终也是走getBean路径详见 依赖注入(DI) 文档.md)。三级缓存解决问题的物质基础循环依赖的提前暴露依赖DefaultSingletonBeanRegistry类中的三个重要属性。这个类负责单例 Bean 的注册与缓存其完整字段与测试用例可参考 Spring-DefaultSingletonBeanRegistry.md。// 一级缓存存放完整 Bean 对象实例化 初始化是最终对外提供的单例 private final MapString, Object singletonObjects new ConcurrentHashMap(256); // 三级缓存存放一个 lambda 表达式ObjectFactory尚未真正创建半成品 private final MapString, ObjectFactory? singletonFactories new HashMap(16); // 二级缓存存放半成品 Bean 对象只实例化、还未初始化即提前暴露的对象 private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16);三者的职责与流转关系可以这样概括缓存数据结构存放内容作用一级singletonObjectsConcurrentHashMap完整单例 Bean最终对外提供实例的正式缓存二级earlySingletonObjectsConcurrentHashMap半成品 Bean提前暴露暂存已从三级缓存中取出的未初始化实例三级singletonFactoriesHashMapObjectFactorylambda延迟创建提前暴露对象只有在真正发生循环依赖时才执行循环依赖问题发生在属性填充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()单例 Beanthis.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 执行时调用的是getEarlyBeanReferenceprotected 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) { // 为解析后的属性值创建一份深拷贝 ListPropertyValue 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递归触发 getBeanresolveReference中有一段核心代码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) { // 第五步从三级缓存获取 ObjectFactorylambda 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)一级缓存没有aa正在创建中二级缓存没有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.mddoCreateBean、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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进