京东一面:Spring 为何需要三级缓存解决循环依赖,而不是二级缓存? 摘要循环依赖是 Spring 面试的高频题而「为什么必须是三级缓存二级缓存为什么不行」更是其中的灵魂追问。本文从循环依赖的定义、单例 Bean 的创建流程、三级缓存的真实作用出发结合 AOP 代理与早期引用问题逐步推导为什么 Spring 最终选择 singletonObjects、earlySingletonObjects、singletonFactories 三级结构并给出源码级分析和大量可运行示例。读完后你会发现三级缓存的核心不是提前暴露「普通 Bean」而是为了在发生循环依赖时仍然能正确暴露「代理对象」。一、这个面试题到底在问什么很多同学背面试题时都会记住一句话Spring 通过「三级缓存」来解决循环依赖分别是singletonObjects、earlySingletonObjects和singletonFactories。但如果面试官继续追问一句「既然提前暴露对象就能解决循环依赖为什么一定要三级缓存我直接用一个一级缓存或者二级缓存不行吗」很多候选人就卡住了。其实这个问题的本质不是让你把三个 Map 的名称背出来而是在考察你是否理解Spring 在创建 Bean 的过程中什么时候可以使用「半成品」什么时候不能。提前暴露的对象究竟应该是原始对象还是可能被代理过的对象。AOP 的动态代理是如何与循环依赖发生耦合的。为什么设计者宁愿引入一层singletonFactories也不愿意只用两个缓存。把这几个问题想清楚你就不再是「背答案」而是能够从设计意图出发把整个机制完整推导出来。本文会带着你从最简单的场景开始一层一层推到最复杂的情况最后再回到源码验证结论。二、先搞清楚什么是循环依赖循环依赖也叫循环引用指的是两个或多个 Bean 之间互相持有对方的引用形成一个闭合的依赖环。例如 A 依赖 BB 又依赖 A这就是一个最简单的循环依赖。在 Java 面向对象设计中循环依赖并不一定就是坏设计但它确实会提高系统复杂度。更重要的是在 Spring 容器初始化 Bean 的过程中如果处理不当循环依赖会直接导致 Bean 创建失败抛出类似BeanCurrentlyInCreationException这样的异常。2.1 循环依赖的典型形式按照依赖环中 Bean 的数量来划分常见的有以下几种自己依赖自己A 内部有一个类型为 A 的属性形成「自引用」。两两互相依赖A 依赖 BB 依赖 A这是面试中最常用的示例。多 Bean 循环依赖A 依赖 BB 依赖 CC 又依赖 A环的长度更长。2.2 按注入方式划分循环依赖能不能被 Spring 解决和注入方式有直接关系构造器注入的循环依赖Spring 无法解决默认直接报错。因为构造器调用发生在对象实例化阶段此时对象必须一次成型无法先创建半成品再后补依赖。Setter 注入或字段注入的循环依赖Spring 可以通过提前暴露「半成品」来解决这是本文讨论的重点。2.3 按 Bean 作用域划分单例 Bean 的循环依赖Spring 可以通过三级缓存解决字段注入场景。原型 Bean 的循环依赖Spring 无法解决因为原型 Bean 每次获取都会创建新实例无法缓存「半成品」供后续复用。把这些前提条件搞清楚后后面讨论三级缓存时才能分清边界。很多同学学习半天还是混乱就是因为没有先分清「什么场景下 Spring 才会启用这套机制」。三、一个最小可复现示例我们先写一个最典型的字段注入循环依赖作为后续分析的锚点。Component public class UserService { Autowired private OrderService orderService; public void login() { System.out.println(UserService#login); } }Component public class OrderService { Autowired private UserService userService; public void createOrder() { System.out.println(OrderService#createOrder); } }在 Spring 容器启动时UserService和OrderService之间形成了循环依赖。如果 Spring 不做特殊处理创建流程会是这样容器开始创建UserService实例。创建完成后需要注入orderService属性。于是容器转去创建OrderService实例。OrderService创建完成后又需要注入userService属性。此时发现UserService还在创建中如果继续往下会陷入无限递归最终栈溢出或直接报错。Spring 为了避免这种死循环会选择在对象实例化完成后、属性还未填充完成时就把这个「早期引用」暴露出去。只要对方拿到这个引用就能完成自己的创建从而打破循环。关键问题是这个早期引用到底应该放在哪里、以什么形式存在四、Spring 解决循环依赖的三个前提在继续深入之前需要再次强调 Spring 解决循环依赖的边界。它不是万能药而是只服务于特定场景。4.1 只能解决单例 Bean 的循环依赖因为单例 Bean 在容器生命周期内只有一份所以可以安全地缓存一个「半成品」。而原型 Bean 不允许被缓存容器创建原型 Bean 时每次都会新 new 一个对象清空当前线程记录Spring 无法保证拿到的早期引用是唯一的所以直接不支持。4.2 只能解决字段注入和 Setter 注入字段注入和 Setter 注入本质上都是「先创建实例再填充属性」。这种两步走的模式天然支持「先暴露半成品后完善属性」。而构造器注入要求所有依赖在构造时就必须就绪无法享受这个机制。4.3 默认允许循环引用Spring 容器可以通过setAllowCircularReferences(false)关闭单例循环依赖的处理。关闭后即使不涉及构造器循环依赖容器也会在检测到循环引用时抛出异常。默认该值为true所以字段注入循环依赖可以被解决。理解这三个前提之后我们再看 Spring 是如何用缓存结构实现「提前暴露」的。五、单例 Bean 的创建流程概要三级缓存机制嵌入在AbstractAutowireCapableBeanFactory#doCreateBean的核心流程中。一个典型的单例 Bean 创建大致分为以下阶段实例化通过构造器或工厂方法创建 Java 对象。此时对象内部的字段还没有注入属于「原始裸对象」。提前暴露判断当前 Bean 是否允许提前暴露如果允许将能创建早期引用的工厂放入第三级缓存。属性填充通过populateBean完成依赖注入Spring 会在这时解析并创建被依赖的 Bean。初始化执行PostConstruct、InitializingBean、BeanPostProcessor 等回调AOP 代理通常也在此前后发生。放入完整单例池Bean 完全就绪后被放入一级缓存singletonObjects。可以看到循环依赖发生的关键窗口在「实例化完成」到「放入一级缓存」之间。这个窗口内的 Bean 就是所谓的「半成品」。Spring 的缓存体系就是管理这些半成品以及它们的最终形态。六、三级缓存分别是什么Spring 中三级缓存定义在DefaultSingletonBeanRegistry中它们本质上就是三个 Map/** 一级缓存存放完整可用的单例 Bean */ private final Map singletonObjects new ConcurrentHashMap(256); /** 二级缓存存放提前暴露的早期单例对象可能是普通对象也可能是代理对象 */ private final Map earlySingletonObjects new ConcurrentHashMap(16); /** 三级缓存存放能创建早期引用的 ObjectFactory 工厂 */ private final Map singletonFactories new HashMap(16);这三层缓存的职责差异是整个机制的核心。下面逐个拆解。6.1 一级缓存 singletonObjects一级缓存中存放的是「已经完全创建成功」的单例 Bean也就是我们平时从applicationContext.getBean(userService)里真正拿到的对象。无论是普通对象还是 AOP 代理后的对象最终完成品都放在这一层。一旦 Bean 进入一级缓存就表示它字段填充完毕、初始化完毕、可能的代理也已经完成。6.2 二级缓存 earlySingletonObjects二级缓存存放的是「已经提前暴露出去」的早期引用。这个对象可能是原始 Bean也可能是已经创建好的代理 Bean。它已经脱离工厂阶段成为可以直接返回给依赖方的成品引用。二级缓存存在的意义是保证在单次创建过程中如果多个 Bean 都向一个半成品索取引用大家拿到的是同一个对象。6.3 三级缓存 singletonFactories三级缓存不是直接存放 Bean而是存放一个ObjectFactory。这个工厂在真正被调用时才会生成并返回「早期引用」。这个延迟创建的特性非常关键因为只有工厂模式才能在真正发生循环依赖时再去决定要不要执行 AOP 代理从而避免不必要的提前代理。可以简单理解为缓存级别名称存什么对象状态一级缓存singletonObjects完整可用 Bean字段已填充、初始化已完成、代理已完成二级缓存earlySingletonObjects提前暴露的早期引用可能是原始 Bean也可能是代理 Bean三级缓存singletonFactories能生成早期引用的工厂Bean 刚被实例化尚未决定是否需要代理七、三级缓存如何协作分步拆解下面以UserService依赖OrderService、OrderService依赖UserService为例完整走一遍流程。7.1 开始创建 UserService容器调用doGetBean(userService)。先去一级缓存找没有。开始创建 UserService通过构造器实例化出原始对象。UserService 是一个单例 Bean允许提前暴露因此 Spring 将「能创建 UserService 早期引用」的工厂放入三级缓存singletonFactories。进入属性填充阶段发现 UserService 需要注入OrderService。7.2 转去创建 OrderService容器调用doGetBean(orderService)。一级缓存、二级缓存、三级缓存都没有 OrderService。于是开始创建 OrderService实例化出原始对象。同样把 OrderService 的早期引用工厂放入三级缓存。属性填充时发现 OrderService 需要注入UserService。7.3 OrderService 向容器要 UserService容器再次执行getSingleton(userService)。一级缓存中没有完整的 UserService。二级缓存中也没有早期 UserService。但是三级缓存中存在 UserService 的ObjectFactory。于是容器调用该工厂得到 UserService 的早期引用。如果 UserService 不需要代理早期引用就是原始对象如果需要代理则此时可能生成代理对象。这个早期引用被放入二级缓存并从三级缓存删除该工厂。OrderService 拿到 UserService 引用后完成自己的字段填充和初始化。OrderService 完全就绪放入一级缓存。7.4 回到 UserService 完成创建UserService 拿到已经完整的 OrderService完成字段填充。UserService 执行初始化并在需要时执行 AOP 代理。如果 UserService 之前已经因循环依赖被提前代理过了这里会复用早期代理而不是重新创建。UserService 完全就绪放入一级缓存。最终整个循环被打破两个 Bean 都成功创建。接下来要回答最关键的问题能不能把这个流程简化成两级缓存八、核心问题为什么不能只用二级缓存很多同学第一次看到三级缓存时会觉得「既然二级缓存里也可以存提前暴露的对象那要三级缓存干嘛在实例化完成后直接把对象放进二级缓存不就行了」这个疑问非常自然也确实曾经在网上引发大量讨论。要彻底回答它必须先引入 AOP 代理这个变量。8.1 如果没有 AOP二级缓存确实似乎够用假设整个应用没有 AOP所有 Bean 都是普通 Java 对象。那么在 Bean 实例化完成后直接把裸对象放进二级缓存其他循环依赖方拿到这个裸对象即可。因为后续字段填充时修改的是同一块内存地址中的对象引用持有者看到的内容也会同步更新。这种场景下确实没有必要引入第三级缓存。也就是说如果只考虑普通 BeanSpring 的两级缓存设计从表面上看就可以工作。面试官追问「为什么要三级」的真正意图往往就藏在后面的 AOP 场景中。8.2 引入 AOP 代理后矛盾出现了Spring AOP 的核心方式是为目标对象生成动态代理。被增强的 Bean 最终暴露给外部的通常是代理对象而不是原始对象。代理对象内部再持有目标对象。这就带来两个问题代理时机问题如果某个 Bean 还没有完成属性填充那它该不该立刻被代理代理对象内部的目标对象还是一个半成品此时创建代理是否合适一致性要求一旦因为循环依赖容器把一个「代理对象」提前暴露给了别的 Bean那么最终放进一级缓存的也必须是这个代理对象。否则就会出现「别人手里拿的是代理而你最终发布的是原始对象」容器内部产生两个不一致的实例。8.3 如果只用二级缓存会怎样假设我们把三级缓存删掉只保留一级缓存和二级缓存。为了在循环依赖时提前暴露对象就必须在实例化完成后立即决定到底往二级缓存里放原始对象还是代理对象。如果选择立即创建代理对象就会导致一个严重违背 Spring 生命周期的问题代理创建得太早。此刻 Bean 还没有经过属性填充也没有执行初始化回调但代理已经被创建并暴露出去。后续容器正常执行initializeBean、执行 BeanPostProcessor 后又可能再走一次代理创建。这样 Spring 需要额外记录并复制大量状态还要处理「提前代理与正常代理之间如何合并」的复杂情况代码会非常别扭也容易破坏 AOP 的统一处理流程。如果选择先放原始对象等到初始化阶段再统一代理那么问题就变成了其他循环依赖方已经拿到了原始对象的引用但最终一级缓存中放的是代理对象。一旦有第三个 Bean 也从一级缓存拿对象它会拿到代理而前面的循环依赖方拿着原始对象。于是容器中实际存在两个不一致的对象AOP 增强在这个提前暴露的场景中相当于失效了。因此只用二级缓存时无论你往二级缓存里放「立即代理好的对象」还是「原始对象」都会遇到要么代理过早、要么代理不一致的问题。8.4 三级缓存如何优雅解决这个问题第三级缓存singletonFactories的精髓就是把「创建早期引用的动作」延迟到真正发生循环依赖的那一刻。Bean 刚实例化完成时Spring 并不会急着判断它是否需要代理也不会立刻把原始对象放进二级缓存而是先通过addSingletonFactory注册一个ObjectFactory。这个工厂内部通常封装了getEarlyBeanReference的相关逻辑。关键点来了如果后续没有发生循环依赖没有任何 Bean 向这个半成品索取早期引用这个工厂就永远不会被调用。Bean 会按照正常生命周期完成属性填充、初始化在最后阶段统一执行 AOP 代理再把代理对象放入一级缓存。此时 AOP 的创建时机和没有循环依赖时完全一样不存在「代理创建过早」的问题。如果发生了循环依赖容器在执行三级缓存查询时才会调用这个工厂。工厂内部可以访问后续的 BeanPostProcessor判断当前 Bean 是否需要代理需要就创建早期代理对象不需要就返回原始对象。随后容器把得到的早期引用升级到二级缓存earlySingletonObjects并删除三级缓存中的工厂。后续无论多少 Bean 再向这个半成品索取引用都会直接从二级缓存拿到同一个对象既保证一致性又避免重复创建代理。最终 Bean 完成初始化时如果它已经被提前代理过Spring 会复用这个早期代理不会再生成第二个代理因此一级缓存和二级缓存可以指向同一个实例。三级缓存就这样把一个「原本必须立刻决策」的问题转变成了「必要时才决策、处理完立刻固化到二级缓存」的延迟机制。九、从源码看三级缓存的读取与升级逻辑上面的结论可以在DefaultSingletonBeanRegistry#getSingleton(String beanName, boolean allowEarlyReference)中得到直接验证。这个方法是 Spring 获取单例 Bean 的核心入口三级缓存的查询顺序非常清晰protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject this.singletonObjects.get(beanName); 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; }从这段代码可以看出三个重要事实。第一一级缓存永远优先。只要singletonObjects中存在完整对象后面的缓存根本不会被查询这也解释了为什么「完整 Bean」和「早期引用」必须区分开来。第二二级缓存只在当前 Bean 正在创建中时才会被读取。也就是说earlySingletonObjects不是普通的全局缓存它专门服务循环依赖场景下的「半成品引用」。第三真正的升级发生在三级缓存命中之后容器调用ObjectFactory.getObject()拿到早期引用随后立即把结果放入二级缓存并移除三级缓存中的工厂。这样下一次再有 Bean 索取同一个早期引用时就不会重复调用工厂也就不会重复创建代理对象。工厂的注册代码addSingletonFactory同样值得关注protected void addSingletonFactory(String beanName, ObjectFactory? singletonFactory) { Assert.notNull(singletonFactory, Singleton factory must not be null); synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } }注册工厂时Spring 不会创建任何代理也不会提前返回引用只是把一个「可能被调用的动作」挂在三级缓存上。由此可见三级缓存并不负责保存对象本身它保存的是「如何按需创建正确早期引用」的策略。十、面试高频追问与易错点理解了核心机制后再看面试中最容易考到的几个追问会更加清晰。10.1 构造器循环依赖为什么无法解决因为构造器注入要求在调用构造方法的那一刻就把所有依赖准备好。此时对象还没有完成实例化连一个可以提前暴露的「内存地址」都不存在容器自然无法把半成品放入缓存。Spring 唯一能做的就是在调用构造器之前进行生命周期判断一旦发现循环依赖就立即抛出BeanCurrentlyInCreationException而不是等到递归栈溢出。应对构造器循环依赖的常见做法是对其中一个依赖加Lazy让容器先注入一个代理等到真正使用时再触发目标 Bean 的创建从而打破构造阶段的强依赖。10.2 为什么不能直接用一级缓存暴露半成品一级缓存是「完成品池」语义非常明确从这里拿到的对象必须是字段填充完毕、初始化完毕、可能已经完成代理的对象。如果允许把半成品直接放进一级缓存就必须同时维护「这个对象是否已经成熟」的额外状态否则其他 Bean 从一级缓存拿到半成品后可能会被误用。此外一级缓存还被大量的正常取用流程频繁访问把半成品和完整品混在一起会让缓存的生命周期边界变得非常模糊。因此 Spring 选择把「半成品引用的临时中转」放到二级缓存把「延迟创建策略」放到三级缓存让一级缓存保持纯粹。10.3 二级缓存在整个机制里的真正作用二级缓存的核心作用是保证「同一次 Bean 创建过程中所有依赖方拿到的是同一个早期引用」。设想一个更长的循环依赖链A、B、C 三个 Bean 互相依赖。如果没有二级缓存每次有 Bean 通过三级缓存工厂拿 A 的早期引用时都可能新创建一个对象或代理最终不同 Bean 手里拿到不同的 A容器状态会彻底混乱。有了二级缓存之后第一次命中工厂的产物会被固化下来后续依赖方直接从二级缓存复用。它既避免了重复调用工厂又保证了一致性。10.4 为什么原型 Bean 不支持循环依赖原型 Bean 的语义是每次获取都创建一个新对象容器无法把这些临时对象安全地缓存起来供后续复用。即使容器的确为原型 Bean 提供了prototypesCurrentlyInCreation这样的线程记录它的作用也只是用于检测循环依赖并报错而不是用于提供提前引用。因此原型 Bean 一旦形成循环依赖Spring 会抛出异常要求开发者主动解开设计上的循环。10.5 三级缓存是不是只能解决 AOP 场景这是一个容易走向极端的理解。严格来说如果不考虑任何特殊的 BeanPostProcessor只依赖最普通的 Bean二级缓存在功能上确实可以覆盖大部分情况。但 Spring 的 Bean 实例化流水线非常开放SmartInstantiationAwareBeanPostProcessor完全可能改变最终暴露给外部的对象形态。AOP 之所以最常被拿出来说是因为它是最典型、面试中最常见的「提前返回代理对象」场景。第三级缓存的本质是为了兼容「早期引用可能需要进行后置处理」这种不确定性而不是仅仅为了应付 AOP。理解了这一点你就能从更通用的角度解释设计动机。十一、总结把答案落到「延迟决策与一致性」上回到最初的问题Spring 为什么需要三级缓存而不是二级缓存答案可以浓缩成一句话三级缓存是为了把「是否提前创建代理对象」的决策从 Bean 刚实例化完成时延迟到真正发生循环依赖的那一刻而二级缓存负责把这个决策的结果固化下来保证所有依赖方拿到同一个早期引用。如果只用一级缓存半成品和完成品会被混为一谈如果只用二级缓存就不得不在实例化完成后立即决定放原始对象还是代理对象。立即放代理会导致代理创建过早破坏 Spring 的初始化流程立即放原始对象又会导致最终发布代理对象之后容器内外出现两个不一致的实例。三级缓存通过singletonFactories的工厂模式解决了这个矛盾。所以面试现场不要只回答「三个 Map 各存什么」而要把论证推进到设计权衡一级缓存保证外部拿到的必须是完整可靠的 Bean。二级缓存保证循环依赖过程中早期引用唯一且可复用。三级缓存保证 AOP 代理等后置处理可以被延迟到真正需要时执行。把这三句话讲清楚再配合getSingleton、addSingletonFactory的源码片段你对这个经典面试题的理解就会比大多数候选人更完整、更有说服力。