尧图精选

Spring循环依赖:三级缓存为何不能省?源码解析AOP代理与Bean创建

🕒 发布时间:2026/9/19 14:32:10 📁 来源:尧图网络
1. 循环依赖这个老问题到底卡在哪一步先聊一个几乎所有做后端的人都会遇到的报错场景项目启动的时候Spring容器正在创建Bean结果直接抛出一句BeanCurrentlyInCreationException: Error creating bean with name a: Requested bean is currently in creation: Is there an unresolvable circular reference?。看到这行日志的时候大部分人的第一反应是去检查是不是 A 依赖了 B、B 又依赖了 A然后开始纠结要不要加Lazy或者把某个依赖改成ApplicationContext.getBean()来绕开。这个报错背后的机制就是 Spring 单例 Bean 的创建流程里为了解决循环依赖而设计的缓存体系。三层缓存——singletonObjects、earlySingletonObjects、singletonFactories——是 Spring 容器在创建单例 Bean 时的三个“停车场”。平时我们写代码感知不到它们的存在可一旦循环依赖出现这三层缓存就成了能不能把 Bean 顺利创建出来的关键。网上关于“三级缓存原理”的文章很多但大多数只停留在“有三层缓存、存什么、取什么”这种表层看完能跑通 Demo却回答不了那个最扎心的问题为什么要搞三级二级到底行不行这篇文章我会从 Spring 源码的视角把这个问题的来龙去脉拆开讲清楚。从容器创建 Bean 的过程、缓存的读写时机、到 AOP 代理和循环依赖如何纠缠在一起最后给出实际排查方法。不看源码也能看懂看完你会明白Spring 这一套设计不是闲得慌每一级都有它存在的理由。2. 三级缓存分别都在干什么2.1 三个 Map 的定位和分工Spring 的DefaultSingletonBeanRegistry里定义了三层缓存这是整个单例 Bean 管理的核心数据结构。去掉注释和并发控制本质就是三个 Map// 一级缓存完整单例Bean的最终存放位置 private final MapString, Object singletonObjects new ConcurrentHashMap(256); // 二级缓存早期暴露的Bean引用此时Bean还没完成完整初始化 private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); // 三级缓存存放Bean的ObjectFactory可以理解为Bean的“生产工厂” private final MapString, ObjectFactory? singletonFactories new HashMap(16);很多文章喜欢用“停车场”来类比但我觉得更像一条“生产流水线”。一级缓存是成品仓库里面的 Bean 属性填完了、初始化方法跑完了、代理也做好了可以直接交给任何调用方使用。二级缓存是半成品暂存区Bean 已经实例化出对象了但是属性可能还没填完初始化方法可能还没执行这个对象是被提前暴露给别人的。三级缓存最特殊它里面存放的是一个ObjectFactory函数式接口不是 Bean 本身。Spring 把“如何创建一个早期引用”的算法封装成了工厂对象存放在这里等真正需要的时候再调用它来生成早期对象。理解三个 Map 的定位是理解整个三级缓存机制的入口。一级是最终归宿二级是临时过渡三级是延迟决策——这个“延迟”非常关键后面讲 AOP 的时候你会看到它到底在等什么。2.2 三级缓存的写入和读取时机三级缓存的写和读在getSingleton和createBean两个核心方法之间有非常清晰的顺序。用代码来感受一下最直接Spring 在创建单例 Bean 时getSingleton(String beanName)这个方法决定了从哪一层拿对象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) { // 关键一步从三级缓存拿到工厂通过工厂生成早期引用 singletonObject singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }再看写入时机。createBean流程走到实例化之后、填充属性之前Spring 会调用addSingletonFactory把当前 Bean 的工厂放入三级缓存protected void addSingletonFactory(String beanName, ObjectFactory? singletonFactory) { synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); } } }这个顺序非常重要先实例化出原始对象然后立刻把“能根据原始对象生成代理对象的工厂”放入三级缓存然后才去填充属性。填充属性时如果发现需要引用其他 Bean就去调getSingleton找那个 Bean如果那个 Bean 也反过来需要当前 Bean就能从三级缓存里找到工厂提前暴露一个引用。Bean 完整创建完成后调addSingleton把成品放入一级缓存同时清理二级和三级缓存中的临时数据protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { this.singletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); } }到这里三层缓存的基本流程就清楚了实例化 → 放入三级缓存 → 填充属性 → 初始化 → 放入一级缓存。中间如果发生循环依赖二级缓存就是那个“被临时征用”的存放点三级缓存负责在最后一刻决定暴露原始对象还是代理对象。3. 二级缓存行不行得从代码推演说起3.1 去掉三级缓存直接暴露原始对象会怎样网络上经常有一种声音“三级缓存是为了解决循环依赖那我把三级缓存去掉用二级缓存直接存原始对象不就行了”这个问题其实隐藏了一个前提就是大多数讨论循环依赖的文章用的是最简单的、没有 AOP 参与的A依赖B、B依赖A场景。在这个场景下二级缓存确实够用。我们来推演一下“一级缓存 二级缓存”的方案。假设 A 和 B 相互依赖且两者都不需要 AOP。没有三级缓存时A 实例化后直接把原始对象放入二级缓存然后填充属性时发现需要 B于是去创建 B。B 实例化后同样把原始对象放入二级缓存填充属性时发现需要 A从二级缓存拿到 A 的原始对象继续完成 B 的创建。B 创建完成后放入一级缓存A 拿到 B 的引用继续完成自己的属性填充和初始化最终放入一级缓存。看起来一切正常全程没有三级缓存参与循环依赖也能解开。那 Spring 为什么不这么做如果只是把三级缓存里的ObjectFactory替换成直接存原始对象就省了一层 Map代码也简单了。问题的关键在于 AOP。一旦 AOP 介入事情就变复杂了。3.2 AOP 代理对象在循环依赖里的特殊要求正常情况下Spring 创建带 AOP 的 Bean 时代理对象是在 Bean 初始化完成后由AbstractAutoProxyCreator在postProcessAfterInitialization阶段创建的。这个过程发生在属性填充之后所有依赖都注入完成初始化方法也执行完了这样代理对象内部的 target 才是一个完整的、可用的原始对象。但循环依赖打破了这个顺序。看一个典型场景A 需要依赖 BB 需要依赖 A并且 A 需要被 AOP 代理。A 实例化后放入三级缓存填充属性时发现需要 B开始创建 B。B 实例化后填充属性时发现需要 A这时候 A 还没走到初始化阶段更没走到创建代理的阶段。如果此时只把 A 的原始对象暴露给 B等 A 的整个流程走完生成一个代理对象放进一级缓存B 手里拿到的还是那个原始对象。这个原始对象没有 AOP 的能力事务、切面逻辑全部失效这是一个非常隐蔽的 Bug。三级缓存正好解决这个问题。singletonFactories里存放的ObjectFactory在执行getObject()时会经过getEarlyBeanReference这个扩展点。SmartInstantiationAwareBeanPostProcessor在这里被调用AbstractAutoProxyCreator正是实现了这个接口才能够在“提前暴露引用”的时刻抢先创建 AOP 代理。回头看那段源码里的关键一行singletonObject singletonFactory.getObject()这一步拿到的可能已经是代理对象了而不是原始对象。所以“二级缓存到底行不行”这句话的正确回答是在纯循环依赖场景下二级缓存只是勉强能用但它把“延迟创建代理”这个灵活性彻底牺牲了。你如果想去掉三级缓存就得把代理创建时间从“初始化之后”提前到“实例化之后”这会改变所有 Bean 的创建顺序引发一堆负面连锁反应。4. 源码里的关键机制为什么工厂比直接存对象更聪明4.1 getEarlyBeanReference 决定代理何时出手三级缓存机制最核心的不是那三个 Map而是getEarlyBeanReference这个扩展点。也就是说三级缓存的价值本质上是一个“回调时机”它让 Spring 有机会在“Bean 被其他人引用之前”完成代理的提前创建。源码在AbstractAutoProxyCreator中是这样实现的Override public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey getCacheKey(bean.getClass(), beanName); this.earlyProxyReferences.put(cacheKey, bean); return wrapIfNecessary(bean, beanName, cacheKey); }关键行为很明确这个 Bean 一旦被提前引用就记录在earlyProxyReferences里同时调用wrapIfNecessary执行代理包装。wrapIfNecessary会判断当前 Bean 是否需要切面、是否符合 Pointcut 规则如果没有命中切面返回原始对象命中切面则返回代理对象。拿到这个代理对象之后Spring 会把它放到二级缓存earlySingletonObjects里并移除三级缓存中的工厂。后续再有人引用这个 Bean直接走二级缓存拿到代理对象不会再重复创建工厂。等到这个 Bean 的完整创建流程走到postProcessAfterInitialization时AbstractAutoProxyCreator会先检查earlyProxyReferences如果发现这个 Bean 已经提前代理过了就直接跳过不再创建第二次代理。这就是为什么不会有“两个代理对象”的问题。4.2 代理时序被三级缓存巧妙拉开如果把三级缓存去掉只留二级缓存并且二级缓存里直接放原始对象代理创建的唯一时机就只剩postProcessAfterInitialization。这在非循环依赖场景下没问题但一旦出现循环依赖早期引用的对象和最终成品对象就会不一致导致 B 拿到的是“未代理”的 A而容器最终保存的是“已代理”的 A。两套对象同时存在于内存里行为还不一样这种问题在线上排查起来极其痛苦。再说把二级缓存改成“直接放代理对象”的方案。如果 Bean 实例化后立刻创建代理那代理内部包装的是一个没有填充属性、没有执行初始化方法的空壳对象。Autowired、Value都还没来得及注入这个代理一放出去被其他 Bean 拿来使用调用任何业务方法都可能因为内部属性为空而抛 NullPointerException。代理对象的 target 必须等所有属性填充完才有意义这是 Spring 容器创建 Bean 的基本顺序。所以三级缓存不是无缘无故多出来的。它把“实例化”和“代理生成”解耦开让代理创建的决策延后到“有 Bean 真正需要引用当前对象”的时刻。如果整个创建链路顺利走完没人提前引用那就把原始对象留在三级缓存里等postProcessAfterInitialization再统一创建代理行为和非循环依赖场景完全一致。如果确实有人提前引用了工厂就能在那一瞬间生成合适形态的引用保证给出去的引用和最终成品是同一个代理。简单说Spring 在一开始不急着做决定而是把决定权推迟到不得不做决定的那一步。这个思想在工程上很常见本质就是一种延迟绑定、按需创建的策略。5. 实操排查循环依赖问题怎么定位和解决5.1 怎么验证三级缓存真的生效了看再多源码都不如亲手验证一次来得有底气。想做实验的话构造一个最简单的循环依赖两个 ServiceA 依赖 BB 依赖 A然后在创建过程中打断点。我推荐三个观察点。第一个是DefaultSingletonBeanRegistry.getSingleton(String beanName, boolean allowEarlyReference)这个方法断点打在里面可以看到一级、二级、三级缓存逐个查找的过程。第二个是DefaultSingletonBeanRegistry.addSingletonFactory观察三级缓存是什么时候写入的A 实例化完成、属性填充之前。第三个是AbstractAutoProxyCreator.getEarlyBeanReference你可以在入口处看到earlyProxyReferences被记录然后观察wrapIfNecessary返回的到底是原始对象还是代理对象。如果只是用 Debug 观察还不够直观可以打开 Spring 的循环依赖日志来跟踪触发顺序。在application.yml里加上下面两行logging: level: org.springframework.beans.factory.support.DefaultSingletonBeanRegistry: DEBUG日志级别调到 DEBUG 后控制台会输出类似Creating shared instance of singleton bean a、Singleton bean creation not allowed while singletons of this factory are in creation之类的信息。结合断点一起看就能清晰还原 A 创建到一半、被 B 拉取引用、B 复用 A 的完整过程。5.2 Spring Boot 2.6 之后默认禁用了循环依赖这是很多刚升级版本的人踩到的坑。Spring Boot 2.6 版本开始官方默认禁止循环依赖哪怕你写的是能正常工作的循环依赖代码启动时也会直接报错。如果你的项目大量依赖循环依赖来组织业务逻辑升级后就得面对一堆报错。处理方式有几种。最快的方式是在配置里显式放开限制spring: main: allow-circular-references: true但这只是治标不治本。我更建议把配置当成一个“过渡方案”真正要做的还是调整代码结构。循环依赖往往是设计上一个信号该拆分了。看看依赖的方向是否真的合理能不能把公共逻辑抽到一个独立的 Service 中或者让其中一方通过构造器注入的方式重新设计。还有两个很实用的替代方案第一种是Lazy注解在注入点加一个延迟加载语义让 Spring 先注入一个代理占位符真正调用时才去获取目标 Bean。这种方式能快速绕开循环依赖问题但也会掩盖真正的结构问题建议谨慎使用。第二种是ObjectProviderT通过provider.getIfAvailable()的方式在运行时手动获取 Bean把硬依赖变成软依赖。这种方案结合业务判断会比较灵活不会直接改变 Bean 的创建时序适合“某个 Bean 只有在特定条件下才需要另一个 Bean”的场景。5.3 常见报错与排查思路速查表实际工作中循环依赖报错的场景其实比想象中多而且不一定都那么直白。我把最常见的几类情况整理成一个速查表方便你按图索骥现象可能原因处理办法启动报BeanCurrentlyInCreationException存在无法通过三级缓存解决的循环依赖比如构造器注入、非单例作用域重构依赖关系改用Lazy或ObjectProvider属性注入的 Bean 是 null依赖关系设计有环且涉及代理提前暴露的对象没有完成属性填充就被使用重新梳理依赖方向避免依赖环检查是否过度使用 AOP升级 Spring Boot 2.6 后启动报错循环依赖被默认禁止先开启allow-circular-references过渡随后逐步重构加了事务注解但切面不生效A 依赖 B、B 依赖 A代理提前创建导致 B 拿到的是非代理引用确认是否真的需要提前代理尝试调整切面范围或调整 Bean 结构同一个 Bean 出现两个不同对象代理对象和原始对象被混用在getEarlyBeanReference和postProcessAfterInitialization打断点确认代理创建时机排查这类问题我的经验是先把配置里各种 Bean 的依赖关系画出来找到形成环的那一段再判断这个环是哪一种类型构造器注入、字段注入还是方法注入。构造器注入形成的循环三级缓存也救不了因为实例化阶段就卡住了字段注入和 setter 注入形成的循环Spring 才有机会用提前暴露的工厂来化解。6. 从缓存设计到工程取舍聊到这里回头看“为什么需要三级缓存二级缓存到底行不行”这个问题答案其实已经摆在面前了二级缓存在没有 AOP 的简单场景下勉强可行但它牺牲了代理创建的灵活性把“延迟决策”变成了“提前决策”一旦 AOP 介入就会出现代理失效或者属性为空这类致命问题。三级缓存通过存放ObjectFactory的方式把创建早期引用的决策延后到真正有人引用的时刻既兼容了正常创建流程也处理了循环依赖的异常场景。我个人在实际排查中最大的体会是不要一遇到循环依赖就想着靠缓存机制去兜底。三级缓存是 Spring 给开发者的一个安全网不是让你随便把代码写成乱麻的理由。真正优雅的工程结构里循环依赖应该是极少数情况。适当用Lazy调整 Bean 获取时机或者把公共逻辑抽离出来都比依赖容器的“特殊能力”更稳妥。实在绕不开的时候至少要知道这一层机制是怎么运作的真出了问题也不至于两眼一抹黑对着那一长串 Stack Trace 发呆。最后再说一个我在团队里经常分享的检查技巧写新模块之前先把各个 Bean 之间的依赖关系和数据流向理清楚尤其是涉及事务、异步、定时任务这些 AOP 能力的时候多花十分钟梳理调用链能省掉后面一整周的排查时间。Spring 的源码设计处处都在为灵活性和稳定性做平衡理解三级缓存不只是为了应付一道面试题更是为了在遇到类似设计决策时能多一层思考的维度。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →