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

在 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
2
3
4
5
6
7
8
9
10
11
@Component
public class A {
@Autowired
private B b;
}

@Component
public class B {
@Autowired
private A a;
}

如果没有三级缓存机制,这样的循环注入是会直接抛异常的。我们来拆开 Spring 是怎么一步一步处理这个问题的:

  1. Spring 准备创建 Bean A,发现它依赖了 B。

  2. 它会先调用 ObjectFactory< A > 把 A 的半成品(还没注入 B)放入(singletonFactories)。

  3. 开始创建 B。

  4. 创建 B 的时候发现它需要注入 A,于是 Spring 开始查缓存:

    • 一级缓存里没有 A。

    • 二级缓存也还没有 A。

    • 但三级缓存有一个 A 的工厂,于是执行这个工厂方法,拿到一个早期的 A,并放进二级缓存。

  5. 继续完成 B 的创建,注入好 A 后,放入一级缓存。

  6. 回头继续完成 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 的小伙伴 🧡