在Spring框架面试中,循环依赖问题及其解决方案是衡量候选人是否深入理解IoC容器核心机制的经典标尺。许多开发者知道“Spring通过三级缓存解决了单例Bean的循环依赖”,但对其中精妙的设计思想、处理流程及边界条件往往语焉不详。深入剖析Spring循环依赖三级缓存解决方案面试,其核心价值在于穿透Spring IoC容器创建Bean的完整生命周期,理解其如何在保证Bean初始化(如AOP代理、@PostConstruct)等复杂逻辑正确执行的前提下,通过巧妙的“提前暴露”与“缓存分级”策略,破解相互依赖的死结,从而展现对Spring框架设计哲学的深刻洞察。本文将为你彻底厘清三级缓存的运作原理与设计智慧。
一、 什么是循环依赖?从一场“先有鸡还是先有蛋”的困境说起

循环依赖是指两个或更多的Bean相互持有对方的引用,构成一个依赖闭环。最常见的是构造函数循环依赖和属性(Setter)循环依赖。
【问题代码示例】**: ```java @Service public class ServiceA { @Autowired private ServiceB serviceB; }
@Service public class ServiceB { @Autowired private ServiceA serviceA; }
当Spring IoC容器启动,尝试创建`ServiceA`时,发现它依赖`ServiceB`;于是转去创建`ServiceB`,又发现它依赖`ServiceA`。此时`ServiceA`尚未创建完成,形成了一个死锁。**Spring默认支持解决单例模式下的属性/Setter注入循环依赖,但不支持构造函数循环依赖**。理解这个前提,是探讨<strong>Spring循环依赖三级缓存解决方案面试</strong>的基础。</p>
<h2>二、 三级缓存架构:破解死结的三把钥匙</h2>
<p>Spring解决循环依赖的核心在于 `DefaultSingletonBeanRegistry` 类中定义的三个Map,它们被称为**三级缓存**:</p>
<pre><code>
// 一级缓存:存放完整的、初始化完毕的单例Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存放早期的、未完全初始化的Bean(已完成实例化,但未填充属性和初始化)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:存放Bean工厂(ObjectFactory),用于生产早期的Bean对象或代理对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
</code></pre>
<p><strong>每一级缓存的核心使命</strong>:<br>
1. **`singletonObjects`(一级缓存)**:这是最终成品的“成品仓库”。一旦Bean走完所有生命周期(实例化、属性填充、初始化),就会被放入这里。我们`getBean`时,首先查的就是这里。<br>
2. **`earlySingletonObjects`(二级缓存)**:这是“半成品暂存区”。存放从三级缓存的工厂中获取到的早期引用(可能是原始Bean,也可能是其代理对象)。它的存在是为了**避免重复从工厂创建对象**,提升性能并保证依赖注入的一致性。<br>
3. **`singletonFactories`(三级缓存)**:这是最关键的“工厂仓库”。在Bean**仅完成实例化(调用了构造函数,但未设置属性)后**,Spring会将一个能生产该Bean早期引用的`ObjectFactory`放入此缓存。这个工厂的核心作用是**在需要注入依赖时,能够智能地返回原始对象或代理对象(如果该Bean需要AOP)**。</p>
<p>在<strong>鳄鱼java</strong>的Spring高级课程中,我们强调:<strong>三级缓存的核心价值不在于“三级”,而在于那个“ObjectFactory”所提供的动态生成与处理能力,它是解耦实例化与初始化、并整合AOP等增强功能的关键</strong>。</p>
<h2>三、 源码级流程拆解:一个Bean的诞生与协作</h2>
<p>我们以`ServiceA`和`ServiceB`的循环依赖为例,结合`AbstractAutowireCapableBeanFactory.doCreateBean`方法,梳理核心步骤:</p>
<p><strong>步骤1:创建ServiceA</strong><br>
1. **实例化**:调用构造函数,在堆中创建一个`ServiceA`对象(此时`serviceB`属性为`null`)。<br>
2. **暴露早期引用(关键!)**:执行`addSingletonFactory`,将一个能返回`ServiceA`(可能是代理)的`ObjectFactory`放入**三级缓存**,并从**二级缓存**中移除`ServiceA`(如果存在)。<br>
3. **属性填充**:Spring发现`ServiceA`依赖`ServiceB`,于是尝试`getBean(“serviceB”)`。</p>
<p><strong>步骤2:转而创建ServiceB</strong><br>
1. **实例化**:调用构造函数,创建`ServiceB`对象。<br>
2. **暴露早期引用**:同样,将`ServiceB`的`ObjectFactory`放入**三级缓存**。<br>
3. **属性填充**:Spring发现`ServiceB`依赖`ServiceA`,于是尝试`getBean(“serviceA”)`。</p>
<p><strong>步骤3:解决依赖,闭环形成</strong><br>
1. 此时获取`ServiceA`,流程如下(`DefaultSingletonBeanRegistry.getSingleton`):<br>
* 从**一级缓存** `singletonObjects`查,无。<br>
* 从**二级缓存** `earlySingletonObjects`查,无。<br>
* 从**三级缓存** `singletonFactories`查,**找到了之前放入的`ObjectFactory`**。<br>
* 调用`ObjectFactory.getObject()`。**如果`ServiceA`需要被AOP代理,这里就会返回代理对象;如果不需要,则返回原始对象**。将得到的这个早期引用放入**二级缓存**,并从**三级缓存**移除该工厂。<br>
* 将这个早期引用(可能是代理)注入给`ServiceB`的`serviceA`属性。<br>
2. **ServiceB继续完成初始化**:`ServiceB`的属性填充完成,接着执行初始化方法(如`@PostConstruct`),然后成为一个完整Bean,被放入**一级缓存**。同时,从二级和三级缓存中清理掉`ServiceB`的记录。</p>
<p><strong>步骤4:回归ServiceA的创建</strong><br>
1. `ServiceB`创建完成后,其引用被返回并注入到`ServiceA`的`serviceB`属性中。<br>
2. `ServiceA`继续完成属性填充和初始化,最终成为一个完整Bean,放入**一级缓存**,并清理其早期缓存记录。</p>
<p>至此,循环依赖成功解决,两个Bean都完成了正确的依赖注入。这个过程完美诠释了<strong>Spring循环依赖三级缓存解决方案面试</strong>中常考的“提前暴露”思想。</p>
<h2>四、 为什么是三级缓存?两级不行吗?</h2>
<p>这是面试中最具区分度的问题。假设只有一级缓存(成品库)和二级缓存(半成品库,直接存放原始对象),会有什么问题?</p>
<p><strong>场景**:如果Bean需要被AOP代理(如使用了`@Transactional`),其最终放入一级缓存的应该是代理对象,而非原始对象。在循环依赖发生时,注入给其他Bean的也必须是这个<strong>最终的代理对象</strong>,否则就会出现:ServiceA注入了ServiceB的原始对象,但Spring容器内部管理的却是ServiceB的代理对象,导致事务等AOP增强失效。</strong></p>
<p><strong>三级缓存如何解决</strong>:<br>
关键在于第三级缓存存放的是`ObjectFactory`。它在被调用(`getObject()`)时,**会执行`SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference`方法**。Spring的AOP代理器(`AbstractAutoProxyCreator`)正是通过此接口方法,在早期暴露阶段就介入并返回代理对象。这样,无论是注入给其他Bean的引用,还是最终放入一级缓存的对象,都是<strong>同一个代理对象</strong>,保证了行为的一致性。</p>
<p><strong>结论**:<strong>三级缓存的核心目的是为了分离“实例化”与“初始化”,并整合Bean生命周期的后置处理器(特别是AOP),确保在循环依赖场景下,所有Bean拿到的都是“最终态”的引用(无论是原始对象还是代理对象)</strong>。二级缓存`earlySingletonObjects`主要起缓存和性能优化作用,避免重复执行工厂逻辑。这是<strong>鳄鱼java</strong>在解析Spring源码时反复强调的设计精髓。</p>
<h2>五、 边界与局限:Spring并非万能</h2>
<p>理解方案的边界与理解方案本身同等重要。</p>
<p><strong>1. 不支持构造函数循环依赖</strong><br>
因为Spring解决循环依赖的前提是**能先调用构造函数完成实例化**,才能暴露`ObjectFactory`。如果是构造函数注入,在实例化阶段就需要完整的依赖对象,此时连“半成品”都无法创建,死结无法打开。Spring会直接抛出`BeanCurrentlyInCreationException`。</p>
<p><strong>2. 不支持非单例作用域(Prototype)的循环依赖</strong><br>
对于原型Bean,Spring容器不缓存它们,每次`getBean`都会创建新实例。因此无法通过缓存早期引用的方式解决循环依赖,同样会抛出异常。</p>
<p><strong>3. 依赖`@Async`等方法注解可能失效</strong><br>
如果循环依赖涉及`@Async`注解,且代理模式为CGLIB(非接口代理),在极端情况下可能因为代理创建时机问题导致异步失效。这通常需要调整代理方式或代码结构。</p>
<p><strong>最佳实践建议</strong>:<br>
* **从设计上避免循环依赖**:循环依赖是代码结构上的“坏味道”,通常意味着职责划分不清。应考虑使用事件发布、回调接口或重构类职责来解耦。<br>
* **如果无法避免,优先使用Setter/字段注入**。<br>
* **警惕构造函数注入导致的启动失败**。</p>
<h2>六、 总结:三级缓存体现的Spring设计哲学</h2>
<p>深入探索<strong>Spring循环依赖三级缓存解决方案面试</strong>,我们收获的远不止一个面试题的答案。它向我们展示了Spring框架如何通过**分层抽象、关注点分离和巧妙的生命周期钩子**,来应对现实世界中的复杂问题。</p>
<p>它将Bean的创建过程精细拆解,利用缓存机制在不同阶段提供不同“视图”的对象,并借助`ObjectFactory`这个抽象层,优雅地集成了AOP等横切关注点。这体现了<strong>“开放-封闭”原则</strong>:对扩展开放(可以插入各种`BeanPostProcessor`),对修改封闭(核心创建流程稳定)。</p>
<p>在<strong>鳄鱼java</strong>看来,学习Spring源码的最大意义,正在于领悟这种解决复杂系统问题的架构思维。下次当你编写`@Autowired`时,不妨想一想背后那场精妙的缓存协作舞蹈。当你的项目启动因循环依赖报错时,你能否迅速定位到是构造函数问题还是作用域问题?技术的深度,决定了你驾驭框架而非被框架驾驭的能力。</p>
版权声明
本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。





