
1. 项目概述从一次线上事故说起那天下午监控告警突然响了一个核心服务在发布后频繁报错错误日志里赫然写着BeanCurrentlyInCreationException。团队里一位刚接手项目不久的同学有点懵他嘀咕着“我明明只是加了个Autowired怎么就把服务搞挂了” 我过去一看果然是两个 Service 互相依赖经典的循环依赖场景。解决这个问题后我问他“你知道 Spring 是怎么处理这个问题的吗为什么需要三级缓存而不是两级” 他想了想说“好像是为了解决代理对象的问题” 对但又不全对。这个问题几乎是 Spring 框架面试的“八股文”常客但真正能把三级缓存的来龙去脉、设计权衡和底层原理讲清楚的人并不多。很多人背下了“一级缓存放成品二级缓存放早期引用三级缓存放工厂”的口诀却说不清为什么这个工厂非得放在第三级合并到第二级行不行今天我们就抛开那些笼统的概念深入到DefaultSingletonBeanRegistry的源码里结合真实的场景和设计抉择把“为什么是三级而不是两级”这个问题彻底掰开揉碎讲明白。无论你是正在准备面试还是在实际开发中想更透彻地理解 Spring 容器的行为这篇文章都会给你一个清晰、深入且能直接用于排查问题的视角。2. 循环依赖的本质与Spring的解决思路在深入缓存机制之前我们必须先达成一个共识什么是循环依赖以及为什么它是个“问题”。2.1 循环依赖的典型场景与困境假设我们有两个简单的 Spring BeanService public class ServiceA { Autowired private ServiceB serviceB; } Service public class ServiceB { Autowired private ServiceA serviceA; }这就是一个典型的构造器循环依赖虽然这里用的是字段注入但原理相通。Spring IoC 容器在启动时需要创建这些 Bean 的实例。一个朴素的创建顺序会陷入死循环开始创建ServiceA- 发现它依赖ServiceB- 去创建ServiceB。开始创建ServiceB- 发现它依赖ServiceA- 去创建ServiceA。又回到创建ServiceA但发现ServiceA正在创建中还没创建完- 死锁抛出BeanCurrentlyInCreationException。这就像两个人隔着一条河河上只有一条船两个人都说“你先过来把船划给我我才能把东西给你让你划船过来”。没有外部干预这就是个死局。2.2 Spring的核心解法提前暴露引用Spring 解决这个死局的智慧在于“提前暴露引用”。它不再坚持“完全创建好一个完美的 Bean 再提供给其他 Bean 使用”的教条而是允许将一个“半成品”即对象实例已通过反射创建但属性还未填充、初始化方法还未执行的引用提前暴露出来供其他依赖它的 Bean 使用。具体来说Spring Bean 的生命周期关键步骤简化如下实例化Instantiate调用构造方法在堆内存中创建一个“空壳”对象。此时对象里的Autowired字段全是null。属性填充Populate为这个“空壳”对象注入它依赖的其他 Bean。初始化Initialize调用PostConstruct方法、执行InitializingBean.afterPropertiesSet()等。循环依赖的突破口就在第1步和第2步之间。Spring 在完成第1步实例化之后会立刻把这个“半成品”对象的引用保存起来这就是缓存的核心作用然后继续执行第2步。当另一个 Bean 在创建过程中需要依赖这个“半成品”时Spring 就能把这个引用提供出去让对方的属性注入得以完成从而打破循环。注意这个方案主要针对单例SingletonBean且使用属性注入Setter/Field Injection的场景。对于构造器注入Constructor InjectionSpring 官方是推荐并默认支持循环依赖检测的但一旦检测到就会直接抛出异常因为构造器注入要求依赖在对象构造时就完全就绪无法使用“提前暴露半成品”的模式。这也是为什么 Spring 官方文档更推荐使用构造器注入的原因之一——它能让循环依赖问题在启动时就暴露出来而不是在运行时因代理等问题出现更诡异的行为。2.3 为什么不能只用一级缓存既然提前暴露引用就能解决问题那是不是用一个 Map我们姑且称为“一级缓存”存放所有创建好的 Bean包括成品和半成品就行了比如MapString, Object singletonObjects new ConcurrentHashMap(); // 一级缓存设想答案是否定的。这样会引发一个严重的问题脏读。假设线程 T1 正在创建 Bean AT1 实例化 A半成品放入singletonObjects。T1 开始为 A 注入属性发现需要 Bean B于是去创建 B。此时线程 T2 来获取 Bean A比如通过applicationContext.getBean(“a”)。由于 A 已经存在于singletonObjects中T2 将直接拿到这个属性还未填充、未初始化的半成品 A并使用这必然导致NullPointerException或其他未定义行为。因此我们需要把“成品Bean”和“正在创建中的Bean”区分开。这就是多级缓存设计的起点。3. 三级缓存架构深度拆解Spring 的DefaultSingletonBeanRegistry类中定义了三个核心的缓存容器这便是我们常说的“三级缓存”。3.1 各级缓存的职责与源码定位public class DefaultSingletonBeanRegistry ... { // 第一级缓存存放完整的、初始化完毕的单例Bean。俗称“成品池”。 private final MapString, Object singletonObjects new ConcurrentHashMap(256); // 第三级缓存存放单例工厂对象。这是解决循环依赖和AOP代理的关键。 private final MapString, ObjectFactory? singletonFactories new HashMap(16); // 第二级缓存存放早期的单例对象从工厂中获取的但尚未完成属性填充和初始化的对象。 private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); }第一级缓存singletonObjects角色终极缓存Spring IoC 容器成熟度的标志。内容存放已经完全走完生命周期实例化、属性填充、初始化的 Bean 实例。一旦 Bean 被放入这里就表示它已经是一个“成品”可以被任何客户端安全地获取和使用。访问getBean()方法最终返回的就是这个缓存里的对象。第二级缓存earlySingletonObjects角色临时过渡缓存用于避免重复创建代理对象。内容存放从singletonFactories三级缓存中获取到的“早期引用”。这个对象可能已经被 AOP 代理所包装但它内部的属性可能还没有被填充Autowired的字段可能是nullPostConstruct方法也还没执行。生命周期非常短暂。仅在 Bean 创建过程中为了解决循环依赖而被临时存放。当 Bean 完全初始化后它会从二级缓存移除并最终将成品放入一级缓存。第三级缓存singletonFactories角色工厂方法缓存是解决循环依赖和集成 AOP 的核心枢纽。内容存放的不是 Bean 实例本身而是一个ObjectFactory?函数式接口。这个工厂的getObject()方法被调用时会执行一个关键操作返回该 Bean 的早期引用并且如果需要会在此刻完成 AOP 代理的创建。时机在 Bean刚刚完成实例化调用构造方法之后即将进行属性填充之前Spring 会为这个 Bean 注册一个singletonFactory到三级缓存中。3.2 Bean创建流程与缓存交互时序让我们结合一个简单的循环依赖A - B - A且 A 需要被 AOP 代理的场景来走一遍核心流程开始创建 A实例化 A 对象调用 A 的构造器得到一个a对象。关键动作将生成 A 早期引用的ObjectFactory放入三级缓存singletonFactories。这个工厂内部逻辑是“如果 A 需要代理就返回代理对象如果不需要就返回原始的a对象。”开始为 A 进行属性填充发现需要 B。转而创建 B实例化 B 对象。同样为 B 注册ObjectFactory到三级缓存。开始为 B 进行属性填充发现需要 A。B 获取依赖 AB 调用getBean(“a”)获取 A。Spring 首先在一级缓存singletonObjects找没有。然后在二级缓存earlySingletonObjects找还没有。接着在三级缓存singletonFactories中找到了 A 的工厂。调用工厂的getObject()。这是决定性的一步因为 A 被定义了 AOP 切面工厂方法会立即执行 AOP 代理创建逻辑生成一个 A 的代理对象aProxy。将aProxy放入二级缓存earlySingletonObjects并从三级缓存中移除 A 的工厂。B 成功拿到aProxy虽然它还是个属性未填充的半成品注入到自己的属性中。至此B 的依赖解决。B 完成创建B 继续完成属性填充、初始化成为一个完整 Bean。将成品 B 放入一级缓存singletonObjects。A 继续创建此时流程回到 A 的属性填充阶段它需要获取 B。getBean(“b”)直接从一级缓存拿到成品 B注入。A 继续完成自身的属性填充和初始化。注意此时 A 的实例是aProxy从二级缓存获得初始化方法会在代理对象上执行。A 完全初始化后将成品aProxy放入一级缓存singletonObjects并清理二级缓存中关于 A 的条目。3.3 核心方法getSingleton的流程剖析上述流程的核心体现在DefaultSingletonBeanRegistry.getSingleton(String beanName, boolean allowEarlyReference)方法中。它的逻辑清晰体现了三级缓存的协作protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 1. 首先从一级缓存成品池获取 Object singletonObject this.singletonObjects.get(beanName); // 如果没拿到并且这个Bean正在创建中标记在另一个集合里 if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 2. 其次从二级缓存早期引用获取 singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { // 3. 最后从三级缓存工厂获取 ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { // 调用工厂方法此处可能生成代理对象。 singletonObject singletonFactory.getObject(); // 将结果放入二级缓存并移除三级缓存的工厂 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }这个方法就是 Spring 解决循环依赖的“心脏”。它定义了访问缓存的优先级一级 - 二级 - 三级并且只有在允许早期引用 (allowEarlyReference为 true这在解决依赖时是允许的) 且 Bean 正在创建中时才会触及三级缓存。4. 关键抉择为什么是三级而非两级现在我们来回答最核心的问题。假设我们合并二级和三级缓存只保留一级缓存和一个“早期缓存”这个早期缓存直接存放实例或工厂行不行答案是在单纯解决循环依赖上或许可以但在集成 Spring AOP 时会引入严重的功能性和一致性问题。4.1 场景推演两级缓存下的AOP代理困境让我们设计一个“两级缓存”方案一级缓存成品池singletonObjects合并缓存早期对象/工厂池earlyCache(可以存放实例或ObjectFactory)现在重复上面的A - B - A且 A 需要代理的场景创建 A实例化后将一个能返回 A或 A 代理的ObjectFactory放入earlyCache。为 A 填充属性发现需要 B转去创建 B。创建 B实例化后将其ObjectFactory也放入earlyCache。为 B 填充属性发现需要 A。于是从earlyCache获取 A 的工厂调用getObject()。此时工厂执行创建了 A 的代理对象aProxy1。B 拿到aProxy1注入成功。B 创建完成成为成品放入一级缓存。流程回到 A 的属性填充它需要 B从一级缓存顺利拿到。A 完成属性填充和初始化准备成为成品。关键问题来了此时应该把哪个对象放入一级缓存方案一把原始的a对象放进去。但这会导致 B 中持有的aProxy1和最终容器里的a不是同一个对象破坏了单例的唯一性而且 B 对 A 的调用切面会失效。方案二把aProxy1放进去。但这需要系统在 A 的创建完成时能准确地知道之前因为循环依赖已经为 A 创建过一个代理aProxy1并且要能找回它。在两级缓存架构下earlyCache里可能只存放了工厂调用工厂getObject()后如果工厂本身不保留结果那么aProxy1的引用就丢失了如果工厂保留结果那么它本质上又退化成了一个“二级缓存”只是逻辑更混乱。4.2 三级缓存如何优雅地解决此困境再看 Spring 的三级缓存方案当 B 通过三级缓存工厂首次获得 A 的早期引用aProxy1时这个对象被固定地存放到了二级缓存earlySingletonObjects中。此后任何其他 Bean或者 A 自身创建过程中再次获取自己在需要 A 的早期引用时都会直接从二级缓存拿到同一个aProxy1。这保证了在早期引用阶段所有依赖方看到的是同一个代理对象。当 A 完成初始化后它从二级缓存中取出这个aProxy1执行后续的初始化回调如果有最终将aProxy1放入一级缓存。这保证了从早期引用到最终成品始终是同一个对象代理对象。三级缓存的核心优势在于职责分离与过程控制三级缓存singletonFactories是“决策层”它不直接产出对象而是持有一个“如何产出早期引用”的策略。这个策略里包含了是否创建代理、如何创建代理的逻辑。它的调用时机被精确控制在“第一次有别的Bean依赖它”的时刻。二级缓存earlySingletonObjects是“缓存层”它缓存了“决策层”的执行结果。一旦决策被执行工厂被调用结果就被固化在这里避免同一个 Bean 的代理对象被重复创建确保单例在早期阶段的唯一性。一级缓存singletonObjects是“结果层”存放最终交付的、完全成熟的单例。如果只有两级将“决策”和“缓存”合并就会面临上述的困境要么无法保证早期引用的唯一性要么需要在合并的缓存中引入更复杂的状态管理逻辑反而失去了清晰性。实操心得理解这个区别对于排查一些诡异的 AOP 失效问题至关重要。例如如果你在PostConstruct方法里进行了一些自调用而切面没有生效很可能就是因为在这个时间点Bean 的代理对象已经存在于二级缓存但自身的某些部分还未就绪所导致的。这时你需要检查你的切面表达式和 Bean 的生命周期。5. 源码层面的证据与关键逻辑让我们从 Spring 源码 (DefaultSingletonBeanRegistry和AbstractAutowireCapableBeanFactory) 中寻找关键佐证。5.1addSingletonFactory– 三级缓存的注册点在AbstractAutowireCapableBeanFactory.doCreateBean方法中创建 Bean 实例后的关键一步protected Object doCreateBean(...) { // 1. 实例化Bean BeanWrapper instanceWrapper createBeanInstance(beanName, mbd, args); Object bean instanceWrapper.getWrappedInstance(); // 2. 【关键】判断是否允许早期暴露单例、允许循环引用 boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 3. 【核心】将Bean的ObjectFactory添加到三级缓存 addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); } // 4. 后续进行属性填充、初始化等... }addSingletonFactory方法很简单就是this.singletonFactories.put(beanName, singletonFactory)。而传入的 Lambda 表达式() - getEarlyBeanReference(...)就是那个关键的“决策工厂”。5.2getEarlyBeanReference– AOP代理的创建点getEarlyBeanReference方法是集成 AOP 的入口protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject bean; if (!mbd.isSynthetic() hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp (SmartInstantiationAwareBeanPostProcessor) bp; // 调用后置处理器的getEarlyBeanReference方法 exposedObject ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }对于 Spring AOPAbstractAutoProxyCreator这个后置处理器实现了SmartInstantiationAwareBeanPostProcessor接口。它的getEarlyBeanReference方法会检查当前 Bean 是否需要被代理如果需要则立即创建代理对象并返回如果不需要则返回原始 Bean。这就是为什么代理对象的创建被提前到了“第一次被依赖时”的原因。三级缓存的结构完美地支撑了这个设计工厂在三级缓存调用工厂产生代理并存入二级缓存后续所有访问都从二级缓存取同一个代理。5.3 从二级缓存升级到一级缓存当 Bean 完全初始化后在DefaultSingletonBeanRegistry.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); } }6. 特殊场景、局限性与最佳实践理解了原理我们就能更好地应对边界情况和做出正确设计。6.1 构造器注入与循环依赖如前所述Spring 无法解决构造器注入的循环依赖。因为 Bean 在实例化调用构造器时就需要所有构造参数就绪。此时 Bean 的实例还未创建出来更谈不上将“半成品引用”暴露到三级缓存。Spring 会在AbstractBeanFactory.doGetBean中检测这种循环依赖并直接抛出BeanCurrentlyInCreationException。最佳实践优先使用构造器注入。这能强制在项目启动时暴露循环依赖问题迫使你重新审视设计通常能发现不合理的紧耦合。使用Autowired进行字段或 Setter 注入虽然方便但可能掩盖了设计上的缺陷。6.2Async、Transactional等基于代理的注解这些注解也依赖于 AOP 代理。它们的工作原理与前述 AOP 代理完全一致。如果被Async标记的方法 A 调用了同类的方法 B而 B 又调用了 A就可能因为自调用不经过代理而导致循环或预期行为失效。这不是三级缓存的问题而是 AOP 代理机制下自调用的一般性限制通常需要通过AopContext.currentProxy()或调整代码结构来解决。6.3 原型Prototype作用域的Bean三级缓存只针对单例 Bean。对于原型 BeanSpring 容器不管理其完整生命周期每次getBean都会创建一个新的实例因此不存在循环依赖的解决。如果原型 Bean 间存在循环依赖Spring 会直接抛出异常。6.4 如何排查循环依赖问题看异常BeanCurrentlyInCreationException是明确信号。异常信息通常会包含循环链如“Requested bean is currently in creation: Is there an unresolvable circular reference?”并列出涉及到的 Bean 名称。分析依赖链根据异常信息或代码画出 Bean 之间的依赖关系图。重点检查Autowired、构造器、Bean方法参数等注入点。使用工具Spring Boot Actuator 的/actuator/beans端点如果开启可以查看所有 Bean 的依赖关系。在 IDEA 等 IDE 中也可以使用插件或内置的 Spring 依赖图分析工具。根本解决重新设计审视循环依赖是否必要。通常可以通过提取公共逻辑到第三个 Bean、使用接口隔离、应用依赖倒置原则DIP或事件驱动等方式解耦。使用Lazy在其中一个注入点添加Lazy注解。这告诉 Spring 延迟初始化该依赖先注入一个代理对象在实际第一次调用时才真正触发目标 Bean 的创建从而打破创建时的循环。这是快速修复的常用手段但需理解它只是推迟了问题并未消除依赖本身。使用 Setter/方法注入将构造器注入改为 Setter 注入但这只是让 Spring 的“提前暴露”机制得以工作同样要谨慎使用。注意事项不要滥用Lazy来掩盖所有循环依赖。过度使用会导致应用启动行为难以预测并可能将启动期的问题推迟到运行时增加调试难度。它应作为重构前的临时方案或确有必要的场景如某些交叉依赖的基础配置 Bean下的选择。7. 总结与核心洞见回到最初的问题“Spring 解决循环依赖为什么需要三级缓存而不是两级缓存”根本原因在于三级缓存是一个精巧的、职责分离的设计它不仅能解决对象创建顺序上的循环死锁更重要的是它能与 Spring AOP 等基于代理的增强机制无缝集成保证在整个 Bean 生命周期中从早期引用到最终成品对于需要代理的 Bean所有依赖方看到的都是同一个代理对象从而确保了单例的唯一性和 AOP 功能的一致性。一级缓存是目标存放最终可用的成品。二级缓存是保证保证在 Bean 创建过程中所有依赖方获取到的早期对象可能是代理是同一个实例。三级缓存是工厂将“是否以及如何创建代理”的决策延迟到第一次被需要时并将决策逻辑封装起来。去掉二级缓存就无法保证早期引用的唯一性去掉三级缓存将工厂合并就无法优雅地实现代理创建的延迟决策和与生命周期的集成。三级缓存的结构是 Spring 在灵活性、功能性和一致性之间找到的最佳平衡点。理解这个设计不仅能让你在面试中对答如流更能让你在遇到复杂的 Bean 创建顺序问题、AOP 不生效等场景时拥有直指问题根源的洞察力。下次再看到BeanCurrentlyInCreationException你脑海中应该能清晰地浮现出这三个 Map 是如何协同工作的以及问题可能出在哪个环节。这才是深入理解一个框架的价值所在。