什么是 Spring 的三级缓存?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
循环依赖理解:面试官不仅仅想知道三个缓存分别叫什么,更想知道你是否理解 Spring 为什么要搞三级缓存,以及它是怎么解决循环依赖这个难题的。
-
Bean 生命周期掌握:三级缓存的运作贯穿了 Bean 创建的整个流程,能讲清楚它,说明你对 Spring Bean 的实例化、属性注入、初始化这一条线是清楚的。
-
源码级深度:能说出
getSingleton()、addSingletonFactory()这些关键方法的调用时机,面试官会认为你真的读过源码,而不是只看过博客。
核心答案
先说结论:Spring 的三级缓存是 DefaultSingletonBeanRegistry 中的三个 Map,用于解决 单例 Bean 的循环依赖 问题。
| 级别 | 缓存名 | 类型 | 存的是什么 | 什么时候放进去 |
|---|---|---|---|---|
| 一级 | singletonObjects |
ConcurrentHashMap |
完全初始化好 的 Bean 实例 | Bean 创建完成,走完全部生命周期后 |
| 二级 | earlySingletonObjects |
HashMap |
提前暴露 的 Bean 引用(可能是代理对象) | 发生循环依赖时,从三级缓存拿到后放入 |
| 三级 | singletonFactories |
HashMap |
Bean 的 ObjectFactory(对象工厂) | Bean 实例化后、属性注入前 |
一句话:一级存成品,二级存半成品,三级存的是生产半成品的工厂。
深度解析
一、什么是循环依赖?
先搞清楚问题是什么,再看怎么解决。
@Component
public class A {
@Autowired
private B b; // A 依赖 B
}
@Component
public class B {
@Autowired
private A a; // B 又依赖 A
}
A 创建 → 发现需要 B → 去 create B → 发现需要 A → 去 create A → 又需要 B……死循环了。
Spring 怎么破?核心思路就四个字:提前暴露。A 创建好实例但还没注入属性的时候,先把 A 的引用 "提前暴露" 出去,这样 B 拿到 A 的早期引用后就能完成创建,然后 A 再回头把 B 注入进来,闭环了。
二、三级缓存解决循环依赖的完整流程
拿上面 A 和 B 互相依赖的例子,走一遍完整流程:
上面这个流程是三级缓存的核心运作机制,分六个步骤:
-
步骤 1:Spring 实例化 A(只是
new出来,属性还没注入),然后立刻把 A 的ObjectFactory放进三级缓存。这一步是关键——在属性注入之前就提前暴露了。 -
步骤 2:A 开始属性注入,发现需要 B,于是去获取 B,触发了 B 的创建流程。
-
步骤 3:B 也走同样的流程——先实例化,再把 B 的
ObjectFactory放进三级缓存。 -
步骤 4:B 开始属性注入,发现需要 A。这时候去缓存里找 A,一级没有、二级没有、三级有!执行
ObjectFactory.getObject()拿到 A 的早期引用,然后把 A 从三级缓存移到二级缓存。这里就是循环依赖被打破的地方——B 拿到了 A 的引用(虽然 A 还没完全初始化),不再死循环了。 -
步骤 5:B 完成初始化,放入一级缓存,同时清理二级和三级缓存。
-
步骤 6:回到 A,把 B 注入进来,A 也完成初始化,放入一级缓存。
最终,两个 Bean 都安安稳稳地待在一级缓存里,二级和三级缓存被清空。
三、为什么必须是三级?两级不行吗?
这个问题面试官特别爱追问,很多人卡在这里。
两级缓存在 没有 AOP 的情况下确实够用。但一旦涉及 AOP,就不行了。
原因在于:AOP 代理对象的创建应该在 Bean 初始化完成之后(即在 AbstractAutoProxyCreator 这个 BeanPostProcessor 中完成)。如果没有循环依赖,一切都是正常的——先创建原始对象,初始化阶段再包装成代理对象。
但如果有循环依赖呢?A 还没初始化完,B 就要引用 A 了。这时候 B 拿到的应该是 A 的代理对象还是原始对象?如果 B 拿到的是原始对象,而 A 后续被代理了,那 B 持有的就是一个 "过期" 的引用。
三级缓存存的不是对象本身,而是 ObjectFactory——一个 对象工厂。只有真正发生循环依赖的时候,才会调用工厂方法 getObject() 来决定返回原始对象还是提前生成代理对象。
// AbstractAutowiredCapableBeanFactory 源码(简化)
// 三级缓存放的是 Lambda 表达式
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
getEarlyBeanReference() 内部会检查是否有 AOP 代理需求,如果有,就提前创建代理对象返回。这样 B 拿到的就是 A 的代理引用,和最终 A 放入一级缓存的是同一个对象。
如果只有两级缓存:你必须在 A 实例化后就立刻决定是否创建代理对象——不管有没有循环依赖,都要提前创建。这违反了 Spring 的设计原则:代理应该在初始化阶段创建,而不是实例化阶段。三级缓存通过 ObjectFactory 延迟了这个决策,只在真正需要的时候才提前创建代理。
四、源码验证
核心代码都在 DefaultSingletonBeanRegistry 中:
public class DefaultSingletonBeanRegistry {
/** 一级缓存:完整的单例 Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
/** 二级缓存:早期暴露的 Bean 引用 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>();
/** 三级缓存:Bean 的 ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();
/** 正在创建中的 Bean 名称集合 */
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>());
// 获取单例 Bean(按顺序查三级缓存)
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先查一级缓存
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 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 解决不了?
三级缓存不是万能的,有些场景它确实搞不定:
| 场景 | 能否解决 | 原因 |
|---|---|---|
| setter 注入 / 字段注入的循环依赖 | ✅ 能 | 实例化后提前暴露引用 |
| 构造器注入的循环依赖 | ❌ 不能 | 实例都还没创建出来,没法提前暴露 |
prototype 作用域的循环依赖 |
❌ 不能 | 原型 Bean 不走缓存,每次都新建 |
@Async 标注的 Bean 循环依赖 |
❌ 可能报错 | @Async 的代理在初始化后期才创建,和三级缓存的早期代理可能冲突 |
其中构造器注入的问题,可以通过 @Lazy 来解决:
@Component
public class A {
private final B b;
@Autowired
public A(@Lazy B b) { // 加上 @Lazy,Spring 注入的是代理对象
this.b = b;
}
}
@Lazy 让 Spring 注入一个 B 的懒加载代理,等真正调用 B 的方法时才去创建 B,巧妙绕过了循环依赖。
面试高频追问
-
为什么三级缓存用的是
ObjectFactory而不是直接存对象?- 为了延迟决策。只有在真正发生循环依赖时,才调用
getObject()决定返回原始对象还是代理对象。如果没有循环依赖,这个工厂根本不会被调用,代理对象的创建就留给正常的初始化阶段,保证了生命周期流程的完整性。
- 为了延迟决策。只有在真正发生循环依赖时,才调用
-
二级缓存的作用是什么?能不能去掉?
- 二级缓存保证在多次循环依赖的场景下,始终返回同一个早期引用。假设 A 被 B 和 C 同时依赖,B 先触发三级缓存拿到 A 的早期引用并升级到二级,之后 C 再来拿时直接从二级取就行,不用重复调用工厂方法。如果去掉二级,每次都要执行工厂逻辑,可能产生不一致的代理对象。
-
Spring Boot 2.6 之后默认禁止循环依赖了,你怎么看?
- 对,
spring.main.allow-circular-references默认改成false了。Spring 官方的态度很明确:循环依赖本身就是设计问题,应该从架构层面避免。三级缓存是兜底方案,不应该被当作正常特性来用。
- 对,
常见面试变体
- "Spring 是怎么解决循环依赖的?"
- "为什么要用三级缓存?两级不行吗?"
- "构造器注入的循环依赖能解决吗?怎么处理?"
- "
@Lazy是怎么解决循环依赖的?"
记忆口诀
"一成品、二半成品、三工厂;先实例再暴露,三级升二级,最后全升一。"
总结
三级缓存是 Spring 为了解决单例 Bean 的 setter 循环依赖而设计的机制。一级存成品,二级存半成品引用,三级存生产半成品的工厂。核心流程是:Bean 实例化后立即把 ObjectFactory 放入三级缓存,发生循环依赖时调用工厂拿到早期引用并升级到二级,Bean 完全初始化后进入一级缓存。面试中重点讲清楚两件事:一是完整流程(六步走),二是为什么三级缓存不能省(和 AOP 代理有关)。
