Spring循环依赖与三级缓存:源码级剖析Bean创建过程

发布时间:2026/9/9 6:00:25
Spring循环依赖与三级缓存:源码级剖析Bean创建过程 1. 循环依赖是什么Spring 为什么一开始也拿它没办法1.1 一个最小可复现的循环依赖示例很多人第一次接触循环依赖是在网上看到别人的报错截图然后自己也写了一个 Demo复制出一个诡异的问题。最简单的场景就是这样两个类Service public class UserService { Autowired private OrderService orderService; }Service public class OrderService { Autowired private UserService userService; }启动 Spring 容器时控制台会出现类似的错误BeanCurrentlyInCreationException: Error creating bean with name userService: Requested bean is currently in creation: Is there an unresolvable circular reference?这句话翻译成人话就是Spring 正在创建 UserService 这个 Bean结果在创建过程中发现它要依赖 OrderService于是转头去创建 OrderService结果 OrderService 也依赖 UserService而 UserService 目前还没创建完两边都在等对方完成形成了一个死等。注意这里必须区分两件事代码逻辑层面的互相调用和对象构建阶段的依赖环是两码子事。UserService 的方法里调用 OrderService 的方法OrderService 的方法里又调用 UserService 的方法这在业务上完全可以没问题因为调用栈本身是分层的只要一个线程按顺序执行就不会死锁。真正会让 Spring 崩掉的是“对象实例化阶段”的互相依赖A 要被创建出来就必须先拿到 BB 要被创建出来又必须先拿到 A。这时候两个对象都还没法完成构造整个循环就锁死了。如果用一个生活化的例子来类比就像两个人合作种菜A 说“我要先等 B 把土翻完才能播种”B 说“我要先等 A 把种子拿来才能翻土”两个人站在地头互相等待谁也不肯先动手。Spring 要解决的就是这个互相等待的问题。1.2 Spring Bean 生命周期循环依赖到底卡在哪一步要理解 Spring 为什么能解决一部分循环依赖又为什么解决不了另一部分必须先搞清楚一个 Bean 从无到有会经历哪些阶段。我把标准流程简化成几个核心环节通过扫描或配置注册 BeanDefinition。getBean 入口被触发单例 Bean 先查缓存。实例化createBeanInstance通过构造器或工厂方法在堆上 new 出原始对象。属性填充populateBean把 Autowired、Resource、XML property 等依赖注入进去。初始化initializeBean执行 Aware 回调、BeanPostProcessor 的前置和后置处理、afterPropertiesSet、init-method 等。注册为完整单例放入单例池。循环依赖会卡在第四步的原因在于UserService 在属性填充阶段需要 OrderService所以 Spring 必须先把 OrderService 创建完成而 OrderService 创建到属性填充阶段时又反过来需要 UserService。此时 UserService 的实例化其实已经完成了——对象已经躺在堆内存里了只是它的属性还没赋值、初始化方法还没调。从内存角度看这个半成品对象是真实存在的只是还没有被放进一个“可供其他人查找”的地方。这一步很关键。正因为半成品对象已经存在才让“提前暴露”有了施展空间。如果连对象都还没创建出来比如构造器参数里就需要对方那无论怎么缓存都救不了因为根本没有一个可以被提前拿走的引用。这也是后文会提到“构造器循环依赖无法解决”的根本原因。1.3 真实项目里循环依赖是怎么蔓延出来的说实话大部分循环依赖不是开发者刻意写出来的而是改代码改出来的。我见过几种典型情况。第一种是多模块依赖没有理清。比如用户模块和订单模块互相调用产品一开始只有一个 Service后来拆模块时为了图省事UserService 里直接注入了 OrderServiceOrderService 里为了查询用户信息又反向注入了 UserService。编译期完全没问题等到启动容器错误才突然炸出来。第二种是团队成员各自加需求。本来 A 依赖 BA 和 C 没有关系后来同事在 C 里加了 A 的依赖在 A 里又加了 C 的依赖两个需求并行开发代码合并后循环依赖凭空产生。第三种是历史遗留代码。老项目里大量使用字段注入Service 之间互相引用的关系像蜘蛛网一样平时不起眼等整个系统启动时一次性爆炸。排查这种问题最痛苦的地方在于报错信息只告诉你“某 Bean 正在创建中”你还需要自己去梳理调用链搞清楚谁先创建、谁引发了谁。所以只有理解了 Spring 内部的三级缓存机制才能在看到报错时快速定位。2. 核心机制拆解为什么 Spring 选择用三个 Map 来兜底2.1 三个缓存 Map 各自承担什么角色三级缓存是业内习惯叫法在 Spring 源码里其实就是 DefaultSingletonBeanRegistry 类中的三个成员变量。我先把它们画成一个对照说明。缓存层级变量名类型存放内容生命周期阶段一级缓存singletonObjectsMapString, Object完整的、已经初始化完成的 Bean创建完成之后二级缓存earlySingletonObjectsMapString, Object提前暴露的早期引用可能是原始对象也可能是代理对象半成品阶段三级缓存singletonFactoriesMapString, ObjectFactory?单例工厂用它来生成早期引用实例化完成之后、属性填充之前这里有个容易混淆的点三级缓存放的不是“半个 Bean”而是一个 ObjectFactory。它在这个时刻并没有真正生成返回给下游的对象而是在关键时刻才调用工厂的 getObject 方法去生成。另外还有一个很重要的辅助集合singletonsCurrentlyInCreation它记录当前正在创建中的 Bean 名字。每当 Spring 开始创建一个单例 Bean就会把名字加入这个集合创建完成后再移除。如果没有这个集合Spring 根本不会知道“当前这个 Bean 正处于创建过程中”也就无法启动后续的早期引用搜索逻辑。整个数据流向是这样的Bean 实例化完成后先把自己封装成一个 ObjectFactory放进第三级缓存。当容器发现有循环依赖需要提前拿到这个 Bean 的半成品时会通过第三级缓存的 ObjectFactory 生成一个早期引用同时放入二级缓存并移除三级中的工厂。所有属性填充和初始化都完成后Spring 把完整对象放入一级缓存同时移除二级缓存中的早期引用。所以本质上一级缓存是“最终成绩单”二级缓存是“临时借给别人的中间产物”三级缓存是“可生成临时产物的工厂”。2.2 getSingleton 的查询逻辑一级、二级、三级是如何串起来的了解数据结构只是第一步真正关键的是 DefaultSingletonBeanRegistry 的 getSingleton 方法。这里有一个带 allowEarlyReference 参数的版本它就是解决循环依赖的核心入口。我摘录一段我最常用来解释的简化源码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 并执行 getObject() ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { singletonObject singletonFactory.getObject(); // 生成早期引用后存入二级缓存同时移除三级缓存 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }读这段源码时有几个点要格外注意。第一查除一级缓存前必须先判断 isSingletonCurrentlyInCreation(beanName)。这个判断非常重要如果不满足就说明当前 Bean 压根不是“因为创建到一半才找不到”而是“本身就不存在或已经创建完成”这时候应该走正常创建流程而不是提前引用逻辑。第二从三级缓存取出 ObjectFactory 并执行 getObject 之后会把生成的对象放进二级缓存、从三级缓存移除。为什么要这样设计因为 ObjectFactory 的执行是有成本的如果没有二级缓存每次都去调 getObject就可能导致一个半成品对象被反复创建甚至产生多个代理对象不一致的问题。所以一旦生成过早期引用就把它固定在二级缓存里后续直接复用。第三getSingleton(beanName, false) 这种调用方式下allowEarlyReference 是 false那就不会走第三级缓存即使三级里有工厂也不会用。这样做的目的是在某些不希望提前暴露的场景下阻断取用。2.3 为什么必须用三级缓存二级到底差在哪里这是面试中最容易被追问的问题既然二级缓存已经能放一个半成品对象为什么还需要第三级我的理解是这样的如果不考虑 AOP 代理二级缓存其实够用。Spring 在 Bean 实例化完成后直接把对象放进二级缓存B 填充属性时从二级缓存里把 A 的半成品拿出来用完全可行。但现实的 Spring 世界里大量 Bean 在初始化阶段会被 BeanPostProcessor 处理最常见的就是 AbstractAutoProxyCreator 这类后置处理器。Spring 的 AOP 代理默认发生在 Bean 初始化完成后的 postProcessAfterInitialization 阶段。举个具体例子如果一个 Bean 上有 Transactional 或者 Aspect 切面那么它最终放进一级缓存的对象通常不是原生对象而是一个 JDK 动态代理或 CGLIB 代理对象。现在假设只保留二级缓存会有问题吗我们来推演一下实例化 A 之后容器把原生 A 放进二级缓存。属性填充阶段B 依赖 A于是从二级缓存拿到了原生 A。后来 A 全部初始化完成Spring 在 A 上执行切面逻辑生成代理 A最后把代理 A 放进一级缓存。这时问题就出现了B 里持有的还是原生 A而容器最终的成品是代理 A。如果切面逻辑靠代理拦截才能生效B 调用 A 的方法时会绕过所有切面这是严重 bug。那如果换一种方案实例化 A 之后不等有没有人需要就立刻对 A 做 AOP 代理把代理 A 放进二级缓存行吗这样又会让所有 Bean 在实例化后都提前生成代理哪怕根本没有循环依赖、也没有被任何对象提前引用白白增加开销。而且有些 Bean 是否应该被代理跟它是否完成初始化状态有关早期强制代理可能产生副作用。三级缓存解决得就很优雅第三级缓存存的是一个 ObjectFactory这个工厂内部会调用 getEarlyBeanReference 方法。当且仅当真的有另一个 Bean 需要提前拿 A 的时候工厂才会执行此时 Spring 才判断“A 是否需要提前 AOP 代理”。如果需要就返回代理对象如果不需要就返回原生对象。这样既避免了所有 Bean 无条件提前代理也保证了 B 拿到的对象和容器最终成品是同一个引用。用一句话概括第三级缓存的核心作用是“延迟 AOP 代理的决策时机”。Spring 不是在一开始就决定某个 Bean 要不要生成代理而是等确认它确实要被循环依赖引用时再决定。这正是第三级缓存存在的最大价值。2.4 三级缓存生效的三个前提条件三级缓存并不是万能钥匙它要生效有几个硬性前提。第一Bean 必须是单例。Spring 的单例池机制只对单例 Bean 生效Prototype 作用域的 Bean 每次都创建新实例不存在一个“当前正在创建中的对象”可以被反复引用的概念所以循环依赖一旦涉及 prototype会直接报错。第二循环依赖必须发生在属性填充阶段。也就是实例化确实已经完成Bean 已经被 addSingletonFactory 放入三级缓存之后。如果依赖发生在构造器阶段spring 还没有实例化完成三级缓存里根本没有这个 Bean 的工厂自然救不了。第三不能存在无法被“破坏”的外部条件。比如循环依赖链条里有 Async 的 Bean或者这个 Bean 需要依赖某种特殊代理处理起来都可能出现奇怪的问题这部分我在后面第 4 节详细讲。3. 源码级追踪一个半成品 Bean 是怎么被“借”出去的3.1 doCreateBean 中三级缓存的注册时机当容器进入 doCreateBean 方法后会执行一个非常关键的逻辑。下面是 AbstractAutowireCapableBeanFactory.doCreateBean 中的一段节选boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); }这里有几个信息很关键。第一earlySingletonExposure 本质上是三个条件的与关系Bean 是单例容器允许循环依赖这个 Bean 确实还在创建过程中。Spring 存在一个 allowCircularReferences 参数默认是 true但如果被设置为 false就相当于从源头关闭了循环依赖支持。第二addSingletonFactory 会把一个 lambda 表达式放入第三级缓存。这个 lambda 包装了 getEarlyBeanReference(beanName, mbd, bean) 方法。所以放进三级缓存的并不是最终要暴露的那个对象而是一个延迟生成的逻辑。第三这段代码执行的位置是 Bean 实例化完成之后、属性填充之前。也就是说只要 Bean 是通过无参构造器或普通构造方式 new 出来的Spring 能保证在 populateBean 之前把这个工厂注册好。但如果是构造器参数里需要别的 Bean那 Spring 在 createBeanInstance 阶段就会陷入死循环根本走不到 addSingletonFactory 这一步。3.2 完整时间线UserService 和 OrderService 互相依赖时发生了什么下面我以一个虚构但典型的过程把 A 和 B 互相依赖的完整创建过程列出来。假设容器先创建 UserService我们叫它 AA 依赖 OrderService我们叫它 B。步骤容器动作二级缓存状态三级缓存状态1开始创建 AA 实例化完成空存放 A 的 ObjectFactory2A 进入属性填充发现需要 B空存放 A 的 ObjectFactory3容器开始创建 BB 实例化完成空A、B 的 ObjectFactory4B 进入属性填充发现需要 A空A、B 的 ObjectFactory5B 填充 A 时 getBean(A)发现 A 正在创建中从三级取出 A 的早期引用删除 A 的工厂6B 拿到 A 的早期引用继续完成自身属性填充和初始化存 A 的早期引用存 B 的 ObjectFactory7B 初始化完成移入一级缓存存 A 的早期引用无 B8A 继续属性填充从容器获取完整的 B存 A 的早期引用无 B9A 属性填充完成执行初始化移入一级缓存删除 A 的早期引用无第 5 步是整个流程最关键的一步。B 在属性填充阶段需要 A于是调用依赖解析最终走到了 getBean(A)。此时容器发现 A 虽然还没有初始化完成但它已经注册了一个 ObjectFactory 在三级缓存里于是调用 getObject 生成 A 的早期引用再放入二级缓存同时移除三级缓存。B 拿到 A 的早期引用后它自己的创建过程就能继续往下推进。等 B 成为完整单例A 再拿走 B 的完整对象最后完成 A 自己的生命周期。整个过程就像两个人合伙干活A 先给了 B 一个“半成品订单”B 拿着这个订单完成自己的任务然后又给 A 提供 B 的完整成果A 最后补完自己的剩余工作。3.3 getEarlyBeanReference 是如何和 AOP 代理联动的三级缓存里的 ObjectFactory 最终会调用 getEarlyBeanReference 方法。这个方法在 AbstractAutowireCapableBeanFactory 中的逻辑是遍历所有 SmartInstantiationAwareBeanPostProcessor让它们有机会对早期引用进行处理。protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject bean; if (!mbd.isSynthetic() hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject bp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject; }Spring AOP 中的 AbstractAutoProxyCreator 就实现了 SmartInstantiationAwareBeanPostProcessor。它的 getEarlyBeanReference 长这样Override public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey getCacheKey(bean.getClass(), beanName); this.earlyProxyReferences.put(cacheKey, bean); return wrapIfNecessary(bean, beanName, cacheKey); }这段代码有两个点值得注意。earlyProxyReferences 这个 Map 记录了“哪些 Bean 已经在早期被代理过了”。wrapIfNecessary 是真正创建 AOP 代理的方法。如果某个 Bean 需要生成代理这里就会返回一个代理对象如果不需要会原样返回原生对象。那么问题来了如果一个 Bean 在早期被代理了它初始化完成之后会不会再被代理一次答案是不会。原因在 AbstractAutoProxyCreator.postProcessAfterInitialization 里有判断Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean ! null) { Object cacheKey getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) ! bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }如果这个 Bean 的原始对象之前被记录在 earlyProxyReferences 中说明它已经在 getEarlyBeanReference 阶段被代理过了那么 remove(cacheKey) 会返回之前记录的原始对象和当前 bean 是同一个对象这时候条件不成立就不会再走一次 wrapIfNecessary。这样能保证一个 Bean 不会因为循环依赖而被代理两次。假如没有循环依赖这个 Bean 从一开始就不会被提前代理postProcessAfterInitialization 中的 earlyProxyReferences 里没有记录那么在初始化完成后会正常执行代理逻辑。换句话说同一个 Bean只有在被循环依赖提前引用时才会走提前代理路径否则走的是常规后置处理路径。两者最终只会有一个生效。3.4 一个容易踩坑的细节被提前暴露的 Bean 如果不需要代理会怎样可能有人会问如果 A 不需要 AOP 代理那三级缓存返回的是原生对象B 拿到 A 的原生对象A 初始化完成后放进一级缓存的也是同一个原生对象这没问题。如果 A 需要 AOP 代理情况稍微复杂第三级工厂执行 getEarlyBeanReference 时A 已经被代理了一次返回给 B 的是代理对象后续 A 初始化完成后由于 earlyProxyReferences 已经记录了同样一个对象postProcessAfterInitialization 不会再生成新代理最终一级缓存里放进来的还是同一个代理对象。所以 B 拿到的代理对象和容器最终保存的对象是同一个引用里面的方法增强能正常生效。这种“同一个引用”的保证是 Spring 设计上的关键约束。只要有一处不一致就会造成 B 拿到的对象和容器的对象不是同一个轻则缓存数据错乱重则 AOP 切面失效、事务不生效排查起来极其隐蔽。4. 常见问题排查什么情况下循环依赖仍然救不了4.1 快速判断当前报错属于哪一类循环依赖相关的报错看多了基本可以按下面这个表格去定位。现象出现阶段本质原因推荐解法构造器注入的循环依赖启动即报错对象尚未实例化无法提前暴露改 setter/字段注入更推荐 Lazy 或重构prototype 作用域循环依赖启动即报错prototype 不走单例池调整作用域避免 prototype 互相注入Async 方法导致的循环依赖启动或运行期报代理错误早期代理和 Async 代理时机不一致把 Async 抽到独立 BeanSpring Boot 2.6 默认报循环依赖启动即报错官方默认禁止 allow-circular-references设置 allow-circular-references 为 true但建议先优化结构构造器循环依赖是最好认的因为它很“绝情”报错信息里会直接显示两个 Bean 名并且是在实例化阶段就抛出来。我之前遇到过这样一个场景Service public class AService { private final BService bService; public AService(BService bService) { this.bService bService; } }B 的构造器里又需要 AService。这种写法在启动时一定会失败。唯一不会启动失败的方法是给其中一个构造器参数加 Lazy 注解让 Spring 先注入一个延迟解析代理真正调方法时才把目标 Bean 创建出来。例如public AService(Lazy BService bService) { this.bService bService; }技术上是能解但业务上要慎重。因为 Lazy 的本质是“掩盖问题”如果一个对象在构造阶段就需要调用另一个对象的方法你用代理拖延到运行期可能把启动期的问题延迟到请求中才暴露定位成本会变高。4.2 最隐蔽的 Async 循环依赖陷阱很多人知道 setter 循环依赖默认能跑于是认为只要不是构造器循环就没问题。但在实际项目中如果循环依赖里有 Async 这样的“方法级代理增强”就可能出现启动不报错、运行期才出问题的现象。原因得从代理时机说起。Async 的代理是由 AsyncAnnotationBeanPostProcessor 在 bean 初始化完成后通过 BeanPostProcessor 机制生成的它不像 AbstractAutoProxyCreator 那样会参与 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference。如果 A 被 B 提前引用早期引用返回的时候A 根本还没机会生成 Async 代理等 A 完整初始化后才生成异步代理放进一级缓存。这时候 B 持有的早期引用和容器最终对象不一致B 调 A 的 Async 方法时其实走的是非代理的原始对象异步逻辑不会生效。这类问题非常难排查因为它往往表现为“方法没有在异步线程执行”而不是报异常。我在处理线上问题时通常第一步就去看类上是否有 Async、Transactional 这类注解以及它有没有被别的 Bean 循环依赖。如果存在这种交叉最稳妥的办法是调整结构把 Async 抽到一个独立的 Service 中让它不参与循环链条。4.3 Spring Boot 2.6 以后为什么默认禁止循环依赖从 Spring Boot 2.6 开始官方默认把 allow-circular-references 设成了 false。这意味着以前可以正常启动的 setter 循环依赖在升级后可能直接启动失败并提示The dependencies of some of the beans in the application context form a cycle官方这么做的本意其实是告诉大家循环依赖本身是设计上的坏味道不应该成为正常操作的一部分。三级缓存只是历史兼容机制给那些历史包袱比较重的遗留系统找一条活路而不是鼓励大家写循环依赖。如果项目里确实有非常庞大的历史代码短时间内没法一次性改完可以显式开启配置来兼容spring: main: allow-circular-references: true但我一般建议这种开关只能在升级过渡期临时用同时应该把“修复循环依赖”列入技术债逐步清掉。否则每次升级 Spring Boot 大版本都可能暴雷。4.4 我在实际项目中的排查步骤与经验如果代码里已经出现循环依赖我的排查套路通常是三步。第一步先把报错信息里的 Bean 名列出来。多看几次你会发现Spring 报错时通常会指出当前正在创建的 Bean 是谁比如 “Bean named aService is expected to be created but is already in creation”。把这个 Bean 名记下来。第二步利用 IDE 的调用层级搜索反查谁注入了这个 Bean这个 Bean 又注入了谁。手动画一张简单的依赖图。循环链条一般不会太长很快就能找出来。第三步判断这个循环依赖是不是可以安全存在的。如果两个类之间确实没有必要出现双向依赖就抽取第三方 Service、或者用应用事件解耦。如果只是构造器造成的“硬环”优先用 Lazy 降低修改成本。还有一种比较实用的排查技巧如果项目里实现了 ApplicationContextAware 或 ApplicationRunner可以临时在启动后打印 DefaultSingletonBeanRegistry 里的 singletonsCurrentlyInCreation 集合看看哪些 Bean 创建过程互相等待。这个集合是受保护的成员变量可以通过反射拿到。Debug 时观察它能直观看到循环依赖发生时 Spring 到底把哪些 Bean 卡在创建中。5. 深入思考三级缓存设计带来的启发和我的个人体会源码看得越细越会觉得三级缓存这个设计其实挺有味道的。它最核心的思想不是“多一个缓存”而是“延迟决策”。Spring 把一个对象的创建过程拆成实例化、属性填充、初始化三个阶段在有循环依赖的情况下允许下游对象提前拿到一个“可能还不是最终形态”的引用但不急于把这个引用完全固化而是通过 ObjectFactory 延迟决定要暴露原生对象还是代理对象。这种模式在实际工作中也有很大参考价值。我在做一个对接第三方 SDK 的功能时遇到过类似问题多个外部系统之间互相依赖对方的客户端对象但初始化第三方客户端非常昂贵不能每次都重新创建更不能在服务启动阶段就直接把所有客户端全部建好。后来我用了一个类似 ObjectFactory 的思路先注册一个工厂只在某个模块真正需要访问对端时才实例化客户端并且用二级缓存保存已经完成的实例避免重复创建。这个思路本质上跟 Spring 的三级缓存如出一