Spring三级缓存解决循环依赖的原理

36 阅读3分钟

Spring 用三级缓存解决单例 Bean 的循环依赖,本质是:

在 Bean 还没完全初始化完成时,先把“半成品对象的引用”提前暴露出去,让另一个 Bean 能注入进来,从而打破死循环。


一、先说结论(一句话版)

Spring 通过三级缓存:

  1. 提前暴露“早期对象引用”
  2. 允许依赖方先拿到代理或原始对象
  3. 等双方都创建完成后再完成属性填充与初始化

从而解决:
A 依赖 B,B 又依赖 A 的死循环问题(仅限单例、setter/字段注入)。


二、三级缓存分别是什么?

DefaultSingletonBeanRegistry 里:

// 一级缓存:完整成品 Bean
Map<String, Object> singletonObjects

// 二级缓存:提前曝光的半成品 Bean(已实例化,未初始化)
Map<String, Object> earlySingletonObjects

// 三级缓存:对象工厂(用于生成早期引用,支持 AOP 代理)
Map<String, ObjectFactory<?>> singletonFactories

三、循环依赖场景模拟

class A {
    @Autowired B b;
}

class B {
    @Autowired A a;
}

创建顺序假设是 A → B:

第一步:创建 A(未完成)

  1. 实例化 A(new A())
  2. 放入 三级缓存
singletonFactories["A"] = () -> getEarlyBeanReference(A)

此时 A 还没注入 B,也没执行初始化。


第二步:A 需要注入 B → 开始创建 B

  1. 实例化 B
  2. B 需要注入 A → 去容器取 A

查缓存顺序:

1. 一级缓存(没有,A 还没完成)
2. 二级缓存(没有)
3. 三级缓存(有 A 的 ObjectFactory)
  1. 通过 ObjectFactory 拿到 A 的早期引用(可能是代理)
  2. 放入 二级缓存 earlySingletonObjects
  3. 注入到 B 中
  4. B 初始化完成 → 放入一级缓存

第三步:回到 A,继续完成注入

  1. 把完整的 B 注入到 A
  2. A 初始化完成
  3. A 从二级缓存升级到一级缓存
  4. 清除二、三级缓存中的 A

循环打破。


四、为什么要“三级”,不能两级吗?

关键在 AOP 代理

如果只有两级:

  • 只能提前暴露原始对象
  • 但 AOP 需要的是 代理对象

三级缓存存的是:

ObjectFactory -> 生成“可能被增强过的早期引用”

调用:

getEarlyBeanReference(beanName, rawBean)

如果 A 需要被代理(@Transactional, @Async 等),
这里会提前返回 代理对象,保证:

依赖方注入的是“最终一致的代理对象”,而不是裸对象。


五、能解决哪些循环依赖?不能解决哪些?

能解决的(经典面试点)

场景是否支持
单例 + setter/字段注入
单例 + AOP 代理✅(三级缓存)
构造器循环依赖
原型(prototype)循环依赖

构造器为什么不行?

因为构造器阶段:

对象都还没 new 完,没法提前暴露引用

还没实例化就要对方实例,直接死锁。


六、面试标准回答模板

Spring 通过三级缓存解决单例 Bean 的循环依赖:一级缓存存放完全初始化好的 Bean,二级缓存存放提前曝光的半成品 Bean,三级缓存存放用于生成早期引用的 ObjectFactory。在创建 Bean A 时,会先将其 ObjectFactory 放入三级缓存;当 B 依赖 A 时,可通过三级缓存获取 A 的早期引用并注入,从而完成 B 的初始化;待 A 完成属性注入和初始化后,再升级到一级缓存。三级缓存的存在还能保证在循环依赖场景下正确暴露 AOP 代理对象。