Spring框架中的三级缓存机制 | 一场为了解决循环依赖的精妙设计 🔁

Spring框架中的三级缓存机制 | 一场为了解决循环依赖的精妙设计 🔁
GnaixEuy在 Spring 中,我们常常听说「一级缓存」「二级缓存」「三级缓存」,但很少有人能完整地讲清楚它们是干什么的。更别说有时候在项目里遇到循环依赖,还一头雾水。
本篇文章就来聊聊 Spring 的三级缓存,为什么它存在,为什么我们不能轻易“砍掉”其中任何一层。
什么是 Spring 的三级缓存?
Spring 在创建 Bean 的时候,为了支持依赖注入,尤其是处理循环依赖,它在 DefaultSingletonBeanRegistry
中维护了三层缓存结构:
缓存名 | 类型 | 用途说明 |
---|---|---|
singletonObjects | Map<String, Object> |
一级缓存:存放完全初始化好的 Bean 实例 |
earlySingletonObjects | Map<String, Object> |
二级缓存:存放早期暴露(未注入依赖)的 Bean |
singletonFactories | Map<String, ObjectFactory> |
三级缓存:存放用于创建早期 Bean 的工厂方法 |
一句话总结:
Spring 是为了能“抢先一步”把 Bean 暴露出去解决循环依赖,但又不能破坏 Bean 生命周期,因此设计了这套机制。
举个例子:Spring 是怎么处理循环依赖的?
来看一个简单又经典的循环依赖示例:
1 |
|
如果没有三级缓存机制,这样的循环注入是会直接抛异常的。我们来拆开 Spring 是怎么一步一步处理这个问题的:
Spring 准备创建 Bean A,发现它依赖了 B。
它会先调用 ObjectFactory< A > 把 A 的半成品(还没注入 B)放入(singletonFactories)。
开始创建 B。
创建 B 的时候发现它需要注入 A,于是 Spring 开始查缓存:
一级缓存里没有 A。
二级缓存也还没有 A。
但三级缓存有一个 A 的工厂,于是执行这个工厂方法,拿到一个早期的 A,并放进二级缓存。
继续完成 B 的创建,注入好 A 后,放入一级缓存。
回头继续完成 A 的创建,这时候可以顺利注入 B。
✅ 最终,两个 Bean 都完成了注入,循环依赖也就顺利解决了。
那我们能不能砍掉某一级缓存?
这是个很多人都有的疑问,甚至在阅读源码时,会觉得三级缓存有点「绕」。
❌ 如果去掉二级缓存 earlySingletonObjects
创建 B 时,即使你从三级缓存拿到工厂拿到了早期的 A,但它没有存到二级缓存中,接下来的注入又找不到了。这就会导致注入失败或无限递归调用工厂。
❌ 如果去掉三级缓存 singletonFactories
那你连“早期的 A”都拿不到,因为工厂都不暴露了。等于一旦遇到循环依赖,Spring 就完全没办法提前暴露 Bean。
✅ 如果你是手动管理 Bean 的生命周期(比如 @Scope(“prototype”)),那循环依赖确实是无法自动解决的 —— Spring 也会明确抛出异常。
本质上,这套机制就是为了解决循环依赖
我们可以把它类比为一个三道门的流程:
- 第1道门:放进来的是做完了所有流程的成熟 Bean(一级缓存)
- 第2道门:放进来的是长得还没完全的半成品(二级缓存)
- 第3道门:是一道隐秘的工厂入口,能制造出临时用的“影分身”(三级缓存)
通过这种分阶段的暴露机制,Spring 成功地绕过了「鸡生蛋蛋生鸡」的死循环。
小结
Spring 的三级缓存机制,看起来复杂,但它的目标非常明确:
保证单例 Bean 的生命周期完整性,同时允许一定程度上的“提前使用”来解决循环依赖。
你可以理解为这是 Spring 容器设计中的一个典型“工程折中”:既要 AOP、又要自动注入、还要容错循环依赖,就必须这么做。
如果你想深入了解这块内容,建议断点调试 AbstractAutowireCapableBeanFactory#createBean 方法,亲自看一遍缓存的存取过程,收获会很大。
🧠 参考阅读推荐:
*Spring Framework 官方文档(最新版) - Dependency Injection 部分
如果你觉得本文有帮助,欢迎点赞、评论或分享给你身边也在 debug Spring 的小伙伴 🧡