Lesson 57 · Spring 生态核心原理
Spring 循环依赖:三级缓存解决方案
面试官:Spring 是怎么解决循环依赖的?
循环依赖(Circular Dependency):两个或多个 Bean 之间形成闭环的依赖关系。
循环依赖有三种注入方式,Spring 能解决的只有后两种:
| 注入方式 | Spring 能否解决 | 原因 |
|---|---|---|
| 构造器注入 | ❌ 不能 | 对象还没 new 出来,无法放入缓存 |
| Setter 注入 | ✅ 三级缓存 | 对象已实例化,可以提前暴露引用 |
| 字段注入(@Autowired) | ✅ 三级缓存 | 同 Setter,对象已实例化 |
为什么循环依赖会导致问题?
Spring 创建 Bean 的标准流程是 实例化 → 属性填充 → 初始化。循环依赖会让这个流程陷入死循环:
// Spring 尝试创建 A
1. new ServiceA() // 实例化 A(空对象)
2. 填充 A 的属性 → 发现需要 ServiceB
3. new ServiceB() // 实例化 B
4. 填充 B 的属性 → 发现需要 ServiceA
5. new ServiceA() // 又去创建 A → 回到步骤 1 → 无限循环!
问题的根源:创建 A 需要 B,创建 B 又需要 A,形成递归。如果不做任何处理,Spring 容器启动时就会 StackOverflowError。
Spring 的解决方案:三级缓存(Three-level Cache)。通过提前暴露"半成品"Bean 的引用,打断循环依赖的递归链。
三级缓存逐步拆解
Spring 在 DefaultSingletonBeanRegistry 中定义了三个 Map:
public class DefaultSingletonBeanRegistry {
/** 一级缓存:存放完全初始化好的单例 Bean */
private final Map<String, Object> singletonObjects
= new ConcurrentHashMap<>(256);
/** 二级缓存:存放提前暴露的半成品 Bean(已实例化,未填充属性) */
private final Map<String, Object> earlySingletonObjects
= new ConcurrentHashMap<>(16);
/** 三级缓存:存放 Bean 工厂对象(ObjectFactory),用于生成早期引用 */
private final Map<String, ObjectFactory<?>> singletonFactories
= new HashMap<>(16);
}
下面用 A → B → A 的循环依赖场景,逐步推演三级缓存的工作过程:
========== 第一步:创建 A ==========
1. 实例化 A:new ServiceA() → 得到空对象 A'
2. 将 A' 包装成 ObjectFactory,放入 三级缓存:
singletonFactories.put("serviceA", () -> getEarlyBeanReference(A'))
========== 第二步:填充 A 的属性 ==========
3. 发现 A 需要 ServiceB → 去获取 B
4. 实例化 B:new ServiceB() → 得到空对象 B'
5. 将 B' 包装成 ObjectFactory,放入 三级缓存
========== 第三步:填充 B 的属性(关键!) ==========
6. 发现 B 需要 ServiceA → 去获取 A
7. getSingleton("serviceA"):
一级缓存?没有
二级缓存?没有
三级缓存?有!调用 ObjectFactory.getObject()
→ 如果需要 AOP,这里返回代理对象;否则返回 A'
→ 将结果放入 二级缓存,从三级缓存移除
8. B 拿到了 A 的早期引用(A' 或代理),完成属性填充和初始化
9. B 初始化完成 → 放入 一级缓存
========== 第四步:A 完成创建 ==========
10. A 拿到完整的 B → 完成属性填充
11. A 初始化完成 → 放入 一级缓存
12. 清理二级缓存中 A 的早期引用
下面是 getSingleton() 的源码,展示三级缓存的查找逻辑:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// ① 先查一级缓存:成品 Bean
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// ② 查二级缓存:提前暴露的半成品
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 双重检查(防止并发重复创建)
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// ③ 查三级缓存:调用工厂生成对象
ObjectFactory<?> factory =
this.singletonFactories.get(beanName);
if (factory != null) {
// 关键:这里会触发 AOP 代理创建
singletonObject = factory.getObject();
// 升级到二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 从三级缓存移除
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
注意同步块和双重检查的设计:三级缓存的工厂只会调用一次,生成结果后立即升级到二级缓存,后续查找直接从二级缓存返回。
为什么两级缓存不够?—— AOP 代理的难题
如果没有 AOP,两级缓存确实够用。问题出在:当 Bean 需要被 AOP 代理时,其他 Bean 注入的应该是代理对象,而不是原始对象。
// 假设 ServiceA 被 @Transactional 标注,需要生成代理
// 只有二级缓存的情况:
1. 实例化 A → A'(原始对象)
2. 放入二级缓存:earlySingletonObjects.put("serviceA", A')
3. 创建 B,B 需要 A → 从二级缓存拿到 A'(原始对象)
4. A 初始化时生成代理 → ProxyA
5. 问题!B 持有的是 A',而不是 ProxyA → 事务失效!
三级缓存的精妙之处在于:三级缓存存的不是 Bean 本身,而是一个 ObjectFactory(工厂)。这个工厂在 getEarlyBeanReference() 中判断是否需要创建代理:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object actualBean = bean;
if (bean != null) {
// 检查缓存中是否已有早期引用
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (!this.earlyProxyReferences.containsKey(cacheKey)) {
// 关键!遍历所有 SmartInstantiationAwareBeanPostProcessor
// 如果 Bean 需要 AOP 代理,这里会返回代理对象
actualBean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
}
}
return actualBean; // 可能是原始对象,也可能是代理对象
}
构造器注入为什么无法解决循环依赖?
三级缓存的前提是:Bean 必须先完成实例化(new 出来),才能将引用放入缓存。
@Service
public class ServiceA {
private final ServiceB b;
public ServiceA(ServiceB b) { this.b = b; } // 构造器注入
}
@Service
public class ServiceB {
private final ServiceA a;
public ServiceB(ServiceA a) { this.a = a; } // 构造器注入
}
// 创建过程:
1. 要 new ServiceA → 需要 ServiceB 作为参数
2. 要 new ServiceB → 需要 ServiceA 作为参数
3. 要 new ServiceA → ... → BeanCurrentlyInCreationException!
根本原因:构造器注入要求在 new 对象的时候就拿到依赖,此时对象还不存在,无法放入任何缓存。
@Lazy 注解:Spring 不会立即注入真实对象,而是注入一个代理,等到第一次调用方法时才去获取真实 Bean。@Service
public class ServiceA {
private final ServiceB b;
public ServiceA(@Lazy ServiceB b) {
this.b = b; // 注入的是代理,不会触发 B 的创建
}
}
Spring Boot 2.6+ 默认禁止循环依赖
从 Spring Boot 2.6 开始,循环依赖默认被禁止,应用启动时会直接报错:
***************************
APPLICATION FAILED TO START
***************************
The dependencies of some of the beans in the application context
form a cycle:
serviceA
↓
serviceB
↓
serviceA
// BeanCurrentlyInCreationException
Spring 团队认为循环依赖是设计缺陷的信号,不应被默默容忍。禁止循环依赖可以:
- 迫使开发者重新审视类职责划分
- 避免隐藏的初始化顺序问题
- 提高代码可维护性和可测试性
spring:
main:
allow-circular-references: true # 不推荐,仅作为迁移过渡
allow-circular-references=true 来掩盖问题。循环依赖几乎总是意味着类职责耦合,应该通过重构来消除。替代方案与面试总结
消除循环依赖的常用手段:
| 方案 | 做法 | 适用场景 |
|---|---|---|
| 提取公共类 | 将 A、B 共同依赖的部分抽到 C | A、B 共享数据或逻辑 |
| 事件驱动 | A 发布事件,B 监听,解耦直接依赖 | 通知型依赖(如创建后回调) |
| @Lazy | 延迟注入代理对象 | 必须用构造器注入的遗留代码 |
| Setter 注入 + @Autowired(required=false) | 将构造器注入改为 Setter | 依赖非必须的场景 |
| ApplicationContext.getBean() | 运行时动态获取 | 极端情况,不推荐 |
- 是什么:循环依赖指 Bean 之间形成闭环依赖,Spring 通过三级缓存解决 Setter / 字段注入的循环依赖
- 三级缓存:一级 singletonObjects(成品)→ 二级 earlySingletonObjects(半成品)→ 三级 singletonFactories(工厂)
- 为什么三级:二级缓存无法处理 AOP 代理场景。三级缓存通过 ObjectFactory 延迟创建代理对象,确保其他 Bean 拿到的是代理而非原始对象
- 构造器注入:不行,因为构造器注入时对象还未实例化,无法放入缓存。可用 @Lazy 缓解
- Spring Boot 2.6+:默认禁止循环依赖,鼓励通过重构消除设计缺陷