Spring 是如何利用“三级缓存”解决循环依赖的?
什么是循环依赖
循环依赖就是你需要实例化 A 的时候,A 需要实例化 B ,实例化 B 又需要 A 的实例。这就像是一个“死锁”或者“无限递归”的过程。
在 Java 开发中,如果我们写出如下代码,必然会报 StackOverflowError:
1 | public class A { |
但在 Spring 中,使用 @Autowired 进行属性注入时,神奇的事情发生了:
1 |
|
上述代码能完美运行。Spring 究竟施了什么魔法,打破了“鸡生蛋,蛋生鸡”的死循环?
答案在Spring Bean 的生命周期与三级缓存设计中。
Bean 的生命周期
要理解循环依赖,必须先明确 Bean 创建的两个关键步骤(简化版):
- 实例化: 对应方法
createBeanInstance。 执行构造函数new Object()。此时对象已经存在于堆内存,但属性都是 null。这个对象被称为原始对象。 - 属性填充: 对应方法
populateBean。 执行@Autowired注入,将依赖注入进去。如果依赖的对象还没创建,就会暂停当前 Bean 的创建,转而去创建依赖的 Bean。 - 初始化: 对应方法
initializeBean。 执行BeanPostProcessor的后置处理(如 AOP 代理生成)、InitializingBean接口回调等。
Spring 解决循环依赖的核心思想是:
将“实例化”和“属性填充”分开。
当 ServiceA 实例化后,虽然它还不完整(属性没填,初始化没做),但它的内存引用地址已经确定了。Spring 提前把这个 “早期引用” 暴露出来,让 ServiceB 先拿着引用,从而打通闭环。
三级缓存的结构
打开 DefaultSingletonBeanRegistry 类,Spring 使用了三个 Map 来存储 Bean,也就是所谓的“三级缓存”。
DefaultSingletonBeanRegistry.java
1 | // 一级缓存:成品池。存放已经经历了完整生命周期(实例化 -> 属性填充 -> 初始化)的 Bean |
为什么需要三个 Map ?
- 一级缓存:毫无疑问,用于存放最终可用的 Bean。
- 二级缓存:为了保证单例的唯一性。
- 三级缓存:为了解决 AOP 代理问题。
很多人说“为了解决循环依赖”,这不全对。如果仅仅是为了解决循环依赖,两级缓存就够了。第三级缓存是为了解决 AOP 代理问题。
详看下文三级缓存
创建 Bean 的流程
假设我们要创建 Bean A,它依赖 B;而 B 又依赖 A。
此时发生的事
sequenceDiagram
participant Main as Spring容器
participant Cache1 as 一级缓存(Pool)
participant Cache2 as 二级缓存(Early)
participant Cache3 as 三级缓存(Factory)
Note over Main: 1. 开始创建 A
Main->>Main: 实例化 A (new A) <br/> 此时 A 是半成品
Main->>Cache3: add(A 的工厂) <br/> 放入三级缓存
Note over Main: 2. 填充 A 的属性,发现依赖 B
Main->>Main: 开始创建 B
Main->>Main: 实例化 B (new B)
Main->>Cache3: add(B 的工厂)
Note over Main: 3. 填充 B 的属性,发现依赖 A
Main->>Cache1: get(A) -> 没找到
Main->>Cache2: get(A) -> 没找到
Main->>Cache3: get(A) -> **找到了!**
Cache3-->>Main: 执行工厂逻辑,返回 A 的引用
Main->>Cache2: 将 A 放入二级缓存
Main->>Cache3: 删除 A 的三级缓存
Note over Main: 4. B 获取到了 A (半成品)
Main->>Main: B 初始化完成
Main->>Cache1: B 放入一级缓存
Note over Main: 5. A 获取到了 B (成品)
Main->>Main: A 初始化完成
Main->>Cache1: A 放入一级缓存
关键源码:getSingleton
当 B 需要注入 A 时,会调用 getSingleton("a"),这是一个递归查找的过程:
DefaultSingletonBeanRegistry.java
1 | protected Object getSingleton(String beanName, boolean allowEarlyReference) { |
为什么需要三级缓存
如果 A 没有被 AOP 代理,确实二级缓存就够了:
实例化 A -> 放二级 -> 创建 B -> B 取二级里的 A -> B 完成 -> A 完成。
但是,如果 A 被 @Transactional 或 AOP 切面代理了呢?
在 Spring 中,正常的 AOP 代理是在 Bean 初始化后 的 BeanPostProcessor 中完成的。也就是说,代理对象通常是在生命周期的最后一步生成的。
这就出现了一个悖论:
- 正常流程:A 实例化 -> 属性填充 -> 初始化 -> 生成 A_Proxy -> 放入一级缓存。
- 循环依赖流程:A 实例化 -> 属性填充 -> B 需要 A -> B 此时就要拿到 A。
- 冲突点:如果 B 此时拿到的是 A 的原始对象,而最后 A 初始化完变成了
A_Proxy,那么:- 单例池(一级缓存)里存的是
A_Proxy。 - B 对象里引用的却是
原始 A。 - 结果:当你调用
b.getA().method()时,AOP 增强逻辑(如事务)失效。
- 单例池(一级缓存)里存的是
三级缓存的 ObjectFactory 就是为了解决这个“时机”问题。 它提供了一个回调机制。
看一下放入三级缓存的那个 Lambda 表达式,Spring 在实例化 A 之后,立刻将其放入三级缓存:
AbstractAutowireCapableBeanFactory.java
1 | // doCreateBean 方法内 |
这个 getEarlyBeanReference 是核心:
1 | protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { |
结论:
第三级缓存存放的 ObjectFactory 提供了一个 后悔 机制。
- 如果没发生循环依赖,这个工厂根本不会被调用,AOP 依然在生命周期最后正常进行。
- 如果发生了循环依赖(B 需要 A),B 会去三级缓存调用
getObject(),强行触发getEarlyBeanReference,提前把 A 的代理对象创建出来给 B 用。
哪些循环依赖解决不了
Spring 的这套机制不是万能的,以下情况会报错:
构造器注入:
1
2
3
4
public class A {
public A(B b) { ... } // 报错
}原因:Spring 必须先实例化 A,才能放入三级缓存。构造器执行不完,对象就无法实例化,也就没机会放缓存。
解决:使用@Lazy注解。public A(@Lazy B b)。这会注入一个 B 的代理,真正调用时才去容器找 B。@Prototype作用域:
原因:Spring 不会缓存多例(Prototype)对象,每次都 new,自然无法利用缓存机制打破闭环。
Spring Boot 2.6+ 的变化
值得注意的是,从 Spring Boot 2.6.0 开始,默认禁止循环依赖。
如果你的项目中存在循环依赖,启动时会直接报错: Relying upon circular references is discouraged and they are prohibited by default.
官方建议通过重构代码(如提取公共类、使用事件驱动等)来解决。如果非要兼容旧代码,需要在 application.properties 或 yaml 中配置:
1 | spring: |
总结
- 核心手段:Spring 利用 三级缓存 解决了 单例模式 下的 属性注入 循环依赖问题。
- 原理: 通过“实例化”与“初始化”分离,利用 中间态(半成品) 对象来打破引用环。
- 三级缓存:
- 一级:最终成品。
- 二级:提前暴露的 Bean(可能是原始的,也可能是代理的),保证多重循环依赖时的引用唯一性。
- 三级:工厂。本质是为了 在 AOP 场景下,能够延迟决策对象的最终形态(是原始对象还是代理对象),并在出现循环依赖时“被迫提前”创建代理对象。


