什么是 Spring 的三级缓存?
2026年02月03日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
当面试官问出这个问题时,他不仅仅是想听到一个简单的名词解释,更深层的考察点在于:
- 对 Spring Bean 生命周期核心流程的理解深度:你是否清楚 Bean 是如何被创建、初始化并放入容器的,三级缓存是这个过程中的一个关键设计。
- 对循环依赖问题的洞察与解决能力:这是三级缓存存在的根本原因。面试官想了解你是否在实际开发中遇到过循环依赖,以及你是否理解 Spring 解决此问题的精巧设计。
- 对 Spring 框架设计思想的理解:三级缓存体现了 “提前暴露” 和 “空间换时间” 的设计权衡。面试官希望看到你能理解这种权衡背后的考量。
- 对动态代理(AOP)与 Bean 创建交织细节的掌握:这是三级缓存中最精妙也是最难理解的部分,能否理清代理对象与原始对象在缓存中的关系,是区分普通开发者和资深开发者的关键。
核心答案
Spring 的三级缓存是 DefaultSingletonBeanRegistry 类中用于解决 单例模式(Singleton)下 Bean 的循环依赖 而设计的三层 Map 缓存结构。它们协同工作,确保在存在循环引用时,Bean 能正确、高效地完成初始化。
三级缓存分别是:
singletonObjects(一级缓存):存放 完全初始化好的、成熟的 Bean。这是主缓存,从中获取的 Bean 立即可用。earlySingletonObjects(二级缓存):存放 提前暴露的、原始的 Bean 对象(尚未填充属性),用于解决循环依赖。singletonFactories(三级缓存):存放 创建 Bean 的ObjectFactory(对象工厂)。这是解决循环依赖和 AOP 代理问题的关键。
核心解决流程:当两个 Bean(A 和 B)相互依赖时,Spring 在创建 A 的过程中,会将其 对象工厂 放入三级缓存,然后开始填充 B 的属性。发现需要 A,则从三级缓存中通过工厂获取 A 的 早期引用(可能是原始对象,也可能是代理对象),并将其放入二级缓存,同时从三级缓存移除该工厂。这样 B 就能完成初始化,继而 A 也能完成属性填充和初始化。
深度解析
原理/机制
让我们以一个经典的循环依赖为例:AService 依赖 BService,同时 BService 依赖 AService。
创建流程与缓存交互:
- 开始创建 A:Spring 根据配置准备创建
AService实例。 - 实例化 A(非初始化):通过构造器反射调用,在堆内存中创建一个“原始”的
AService对象(此时bService属性为null)。 - 将 A 的工厂放入三级缓存:Spring 将能产生这个原始 A 对象的
ObjectFactory放入singletonFactories(三级缓存)。这是打破循环的关键一步。 - 填充 A 的属性(初始化):Spring 准备为 A 的
bService属性注入值。它发现需要BService。 - 开始创建 B:流程转到创建
BService。 - 实例化 B:同样,创建一个“原始”的
BService对象。 - 将 B 的工厂放入三级缓存:将 B 的
ObjectFactory放入三级缓存。 - 填充 B 的属性:准备为 B 的
aService属性注入值。它发现需要AService。 - 获取 A 的早期引用:
- 首先,从一级缓存 (
singletonObjects) 找,没有。 - 然后,从二级缓存 (
earlySingletonObjects) 找,没有。 - 最后,从三级缓存 (
singletonFactories) 找到 A 的工厂。调用这个工厂的getObject()方法。- 如果 A 不需要 AOP 代理:
getObject()直接返回第 2 步创建的原始 A 对象。 - 如果 A 需要 AOP 代理:
getObject()会执行后置处理器,返回一个 A 的代理对象(这也是为什么需要工厂,而非直接存对象的原因)。
- 如果 A 不需要 AOP 代理:
- 将这个(原始或代理)对象放入二级缓存 (
earlySingletonObjects),并从三级缓存移除该工厂。
- 首先,从一级缓存 (
- B 完成初始化:B 拿到了 A 的早期引用,成功完成属性注入和后续的初始化回调(如
@PostConstruct)。然后将 完整的 B 放入一级缓存,并清理二、三级缓存中关于 B 的条目。 - A 完成初始化:流程回到第 4 步,此时 A 拿到了已完全初始化的 B 对象,注入属性,完成自己的初始化。最后将 完整的 A 放入一级缓存。
为什么是 “三级” 缓存,不是两级?
这是问题的精髓。关键在于 AOP 代理。
- 如果只有两级(一级和二级):我们假设只缓存对象本身。在 A 实例化后,我们不得不立即决定是放入原始对象还是代理对象。但此时可能无法确定后续是否需要 AOP(例如,判断依据的切面信息可能来自后续的 Bean 定义加载)。如果放入了原始对象,但后来发现需要代理,就会导致 B 拿到的是原始对象,而最终容器里是代理对象,对象不一致,引发严重问题。
- 三级缓存(引入
ObjectFactory)的优势:工厂提供了 延迟处理 的能力。只有在真正发生循环依赖、需要提前暴露引用时,才调用工厂创建对象。此时,所有配置都已加载完毕,Spring 可以准确判断是否需要创建代理,从而返回正确的对象(原始或代理)。这是一个非常巧妙的设计。
代码示例
以下是 DefaultSingletonBeanRegistry 中三级缓存定义的简化展示:
public class DefaultSingletonBeanRegistry ... {
// 一级缓存:成熟Bean的家
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:早期曝光对象的临时客栈
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:对象工厂的兵工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 从一级缓存查
Object singletonObject = this.singletonObjects.get(beanName);
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 只能解决单例作用域(Singleton)且采用属性注入(Setter/Field)方式的循环依赖。对于构造器注入导致的循环依赖,Spring 会直接抛出
BeanCurrentlyInCreationException,因为 Bean 在实例化阶段就需要依赖,而那时它自己甚至还没被创建出来,无法提前暴露。 - 尽量避免循环依赖:三级缓存是优秀的“补救”机制,但循环依赖本身就是一种代码设计上的“坏味道”。它增加了模块间的耦合度,使代码的理解和测试变得更复杂。在项目设计中,应尽量通过重构(如提取公共逻辑到第三个类、使用事件/回调、应用设计模式)来避免。
- 多例(Prototype)作用域的 Bean 无法解决循环依赖:因为 Spring 不缓存 Prototype 类型的 Bean,每次请求都会新建,无法提供稳定的早期引用。
- 利用 IDE 或工具检测:现代 IDE(如 IntelliJ IDEA)和静态代码分析工具可以帮你检测出潜在的循环依赖问题。
常见误区
- 误区一:“Spring 用三级缓存解决了所有循环依赖。”
- 纠正:如上所述,只能解决 Singleton + Setter/Field注入 的场景。
- 误区二:“二级缓存就够了,三级缓存是多此一举。”
- 纠正:没有三级缓存(对象工厂),就无法优雅地处理 存在 AOP 代理 场景下的循环依赖,会导致代理对象和原始对象不一致的问题。
- 误区三:“从缓存获取对象的顺序是一级 -> 二级 -> 三级。”
- 纠正:顺序是对的,但仅当该 Bean 正在创建中(
isSingletonCurrentlyInCreation为 true) 时,才会去查询二级和三级缓存。否则,只需查询一级缓存。
- 纠正:顺序是对的,但仅当该 Bean 正在创建中(
总结
Spring 的三级缓存是一个通过 提前暴露对象工厂 来打破单例 Bean 循环依赖的精巧机制,其核心价值在于延迟决策,以统一且高效的方式兼容了普通 Bean 和 AOP 代理 Bean 的创建过程。理解它,是深入掌握 Spring IoC 容器运作原理的重要里程碑。