一、开篇故事:鸡生蛋还是蛋生鸡?🐔🥚
想象这样一个场景:
小明对小红说: "只有你先借我100块,我才能还你之前欠的200块。"
小红对小明说: "只有你先还我200块,我才能借你100块。"
结果:两人都在等对方先行动,陷入死锁!😱
这就是循环依赖问题的本质:A依赖B,B又依赖A,谁都无法先创建完成。
二、什么是循环依赖?🤔
2.1 定义
循环依赖是指两个或多个Bean相互依赖,形成一个闭环。
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB; // A依赖B
}
@Component
public class ServiceB {
@Autowired
private ServiceA serviceA; // B依赖A
}
2.2 图解
ServiceA
↓
依赖 ServiceB
↓
依赖 ServiceA
↓
......
(死循环!)
2.3 生活类比
装修房子的死循环:
- 木工:"等电工布完线,我才能装天花板。"
- 电工:"等木工装完天花板,我才能安装灯具。"
- 结果:两人都在等对方,房子永远装不完!🏚️
三、Spring如何解决循环依赖?—— 三级缓存大法 🎯
3.1 三级缓存机制
Spring通过三级缓存解决循环依赖:
public class DefaultSingletonBeanRegistry {
// 一级缓存:存放完全初始化好的Bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存放早期暴露的Bean(半成品)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存:存放Bean工厂对象(生产线)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}
3.2 三级缓存的角色
| 缓存级别 | 名称 | 作用 | 生活类比 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 存放完全初始化的Bean | 成品仓库 🏢 |
| 二级缓存 | earlySingletonObjects | 存放早期暴露的Bean | 半成品仓库 🏗️ |
| 三级缓存 | singletonFactories | 存放Bean工厂 | 生产线 🏭 |
3.3 解决过程(字段注入)
场景:A依赖B,B依赖A
1. 创建A实例(还未填充属性)
→ 放入三级缓存(singletonFactories)
2. 填充A的属性,发现需要B
→ 去创建B
3. 创建B实例(还未填充属性)
→ 放入三级缓存
4. 填充B的属性,发现需要A
→ 从三级缓存获取A的ObjectFactory
→ 调用getObject()获取A的早期引用
→ A从三级缓存移到二级缓存
→ B注入A的早期引用
5. B完成初始化
→ 放入一级缓存
→ 从二级、三级缓存移除
6. A注入B
→ A完成初始化
→ 放入一级缓存
→ 从二级、三级缓存移除
✅ 循环依赖解决!
3.4 图解过程
时刻1: 创建A
三级缓存: [A的工厂]
二级缓存: []
一级缓存: []
时刻2: 创建B,B需要A
三级缓存: [A的工厂, B的工厂]
二级缓存: []
一级缓存: []
时刻3: B从三级缓存获取A
三级缓存: [B的工厂]
二级缓存: [A半成品]
一级缓存: []
时刻4: B完成
三级缓存: []
二级缓存: [A半成品]
一级缓存: [B成品]
时刻5: A完成
三级缓存: []
二级缓存: []
一级缓存: [A成品, B成品]
四、为什么构造器注入无法解决循环依赖?💣
4.1 问题代码
@Component
public class ServiceA {
private final ServiceB serviceB;
// 构造器注入
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Component
public class ServiceB {
private final ServiceA serviceA;
// 构造器注入
@Autowired
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
4.2 启动报错
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| serviceA defined in file [ServiceA.class]
↑ ↓
| serviceB defined in file [ServiceB.class]
└─────┘
4.3 为什么无法解决?
核心原因:构造器注入时,Bean还没创建出来!
创建A的流程:
1. 调用A的构造器
→ 需要传入参数B
→ 去创建B
2. 调用B的构造器
→ 需要传入参数A
→ 去创建A(又回到步骤1)
3. 死循环!💀
关键问题:
-
字段注入:先创建实例(空对象),后填充属性
✅ 可以先把空对象暴露出去 -
构造器注入:必须先有依赖对象,才能调用构造器
❌ 对象都没创建,无法暴露
4.4 生活类比
字段注入(可解决):
1. 先盖房子框架(创建实例)
2. 再装修内部(填充属性)
→ 框架已经存在,可以先让别人参观
构造器注入(无法解决):
1. 必须先准备好所有建材(依赖对象)
2. 才能开始盖房子(调用构造器)
→ 建材还没准备好,房子根本盖不起来
4.5 源码分析
// AbstractAutowireCapableBeanFactory.doCreateBean()
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) {
// 1. 实例化Bean(调用构造器)
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
// ⚠️ 构造器注入在这一步就需要依赖对象
// 如果依赖对象还没创建,就会触发创建
// 形成死循环!
Object bean = instanceWrapper.getWrappedInstance();
// 2. 提前暴露(字段注入才能走到这一步)
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// 3. 填充属性(字段注入在这一步才需要依赖对象)
populateBean(beanName, mbd, instanceWrapper);
return bean;
}
五、为什么Prototype(多例)无法解决循环依赖?💥
5.1 问题代码
@Component
@Scope("prototype") // 多例
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Component
@Scope("prototype") // 多例
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
5.2 启动报错
Error creating bean with name 'serviceA':
Requested bean is currently in creation: Is there an unresolvable circular reference?
5.3 为什么无法解决?
核心原因:Prototype Bean不会被缓存!
Singleton(单例):
→ 创建一次,放入缓存
→ 下次直接从缓存拿
→ ✅ 可以提前暴露半成品
Prototype(多例):
→ 每次都创建新实例
→ 不会放入缓存
→ ❌ 无法提前暴露
5.4 详细过程
1. 获取A(多例)
→ 创建新的A实例
→ 不放入缓存(因为是prototype)
2. 填充A的属性,需要B
→ 获取B(多例)
→ 创建新的B实例
3. 填充B的属性,需要A
→ 获取A(多例)
→ 又创建新的A实例(不是第1步的A)
4. 无限循环创建新实例!💀
5.5 图解
Singleton(单例):
[缓存]
↓
A实例(唯一)
Prototype(多例):
每次都new
↓
A实例1, A实例2, A实例3, ......(无穷多个)
5.6 源码分析
// AbstractBeanFactory.doGetBean()
if (mbd.isSingleton()) {
// 单例:使用缓存
sharedInstance = getSingleton(beanName, () -> {
return createBean(beanName, mbd, args);
});
} else if (mbd.isPrototype()) {
// 多例:每次都创建新实例,不缓存
Object prototypeInstance = createBean(beanName, mbd, args);
// ⚠️ 没有缓存机制,无法提前暴露
}
六、三级缓存为什么要有三级?🎯
6.1 为什么不是两级缓存?
假设只有两级:
- 一级:成品
- 二级:半成品
问题来了:如果需要AOP代理怎么办?
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB;
@Transactional // 需要创建代理对象
public void doSomething() { }
}
困境:
- 提前暴露原始对象A
- B注入了原始对象A
- 后来A需要创建代理对象A'
- 最终容器中有两个A(原始A和代理A')
- B注入的是原始A,不是代理A'
- 💥 事务失效!
6.2 三级缓存的作用
三级缓存存的是ObjectFactory(工厂):
// 三级缓存存的是lambda表达式
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
// 如果需要AOP,这里会创建代理对象
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject; // 可能是原始对象,也可能是代理对象
}
流程:
1. A创建,放入三级缓存(工厂)
2. B需要A,调用三级缓存的工厂方法
3. 工厂方法判断是否需要AOP
→ 需要:返回代理对象A'
→ 不需要:返回原始对象A
4. A'移到二级缓存
5. B注入A'(代理对象)
6. ✅ 最终B注入的是代理对象,事务生效!
6.3 总结
| 缓存 | 作用 | 为什么需要 |
|---|---|---|
| 一级 | 成品 | 存放完全初始化的Bean |
| 二级 | 半成品 | 存放早期暴露的Bean(可能是代理) |
| 三级 | 工厂 | 延迟决定是否需要代理,保证一致性 |
如果没有三级缓存,AOP会出问题!
七、解决方案 💡
方案1:改用字段注入(推荐)✅
@Component
public class ServiceA {
@Autowired // 字段注入
private ServiceB serviceB;
}
@Component
public class ServiceB {
@Autowired // 字段注入
private ServiceA serviceA;
}
优点: 简单,Spring能自动解决
缺点: 字段不可变性较差,不利于测试
方案2:使用@Lazy延迟注入 ✅
@Component
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(@Lazy ServiceB serviceB) { // 延迟注入
this.serviceB = serviceB;
}
}
@Component
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
原理:
- @Lazy会注入一个代理对象
- 真正使用时才会去获取真实对象
- 打破循环依赖
方案3:Setter注入 ✅
@Component
public class ServiceA {
private ServiceB serviceB;
@Autowired
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Component
public class ServiceB {
private ServiceA serviceA;
@Autowired
public void setServiceA(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
方案4:重新设计(最佳)⭐⭐⭐⭐⭐
// 问题:A和B相互依赖,说明设计有问题
// 解决方案1:提取公共依赖
@Component
public class CommonService {
// 公共逻辑
}
@Component
public class ServiceA {
@Autowired
private CommonService commonService;
}
@Component
public class ServiceB {
@Autowired
private CommonService commonService;
}
// 解决方案2:使用事件驱动
@Component
public class ServiceA {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void doSomething() {
// 不直接调用B,而是发送事件
eventPublisher.publishEvent(new SomeEvent());
}
}
@Component
public class ServiceB {
@EventListener
public void handleEvent(SomeEvent event) {
// 监听事件
}
}
方案5:使用@PostConstruct ✅
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB;
@PostConstruct
public void init() {
// 在这里使用serviceB
}
}
八、循环依赖解决能力总结表 📊
| Bean作用域 | 注入方式 | 是否能解决 | 原因 |
|---|---|---|---|
| Singleton | 字段注入 | ✅ 能 | 三级缓存 |
| Singleton | Setter注入 | ✅ 能 | 三级缓存 |
| Singleton | 构造器注入 | ❌ 不能 | 实例还未创建 |
| Prototype | 字段注入 | ❌ 不能 | 没有缓存 |
| Prototype | Setter注入 | ❌ 不能 | 没有缓存 |
| Prototype | 构造器注入 | ❌ 不能 | 没有缓存 |
| Singleton + Prototype | 任何 | ❌ 不能 | Prototype不缓存 |
总结规律
能解决:
- ✅ 单例 + 字段注入
- ✅ 单例 + Setter注入
- ✅ 单例 + 构造器注入 + @Lazy
不能解决:
- ❌ 构造器注入(无@Lazy)
- ❌ Prototype(任何注入方式)
- ❌ 单例依赖Prototype
九、面试高频问题 🎤
Q1: Spring如何解决循环依赖?
答: 通过三级缓存机制:
- 一级缓存存成品Bean
- 二级缓存存早期暴露的Bean
- 三级缓存存Bean工厂
在Bean创建过程中,先实例化,然后提前暴露到三级缓存,再填充属性。如果有循环依赖,可以从三级缓存获取早期引用,打破循环。
Q2: 为什么构造器注入无法解决循环依赖?
答: 因为构造器注入时,Bean实例还未创建,无法提前暴露。而三级缓存的前提是Bean实例已经创建(至少是个空对象),才能暴露出去。
Q3: 为什么需要三级缓存,二级不够吗?
答: 为了支持AOP。三级缓存存的是ObjectFactory,可以在真正需要时才决定是否创建代理对象,保证注入的Bean和最终容器中的Bean是同一个(可能都是代理对象)。
Q4: Prototype为什么无法解决循环依赖?
答: 因为Prototype Bean每次获取都创建新实例,不会缓存。没有缓存就无法提前暴露,所以无法解决循环依赖。
Q5: 如何避免循环依赖?
答:
- 重新设计,避免相互依赖
- 提取公共依赖到第三个类
- 使用事件驱动代替直接调用
- 使用@Lazy延迟注入
- 改用字段注入或Setter注入
十、最佳实践建议 💡
1. 优先使用构造器注入
// ✅ 推荐(有循环依赖时加@Lazy)
@Component
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
}
理由:
- 字段可以是final,不可变
- 依赖关系明确
- 方便单元测试(不需要Spring容器)
2. 避免循环依赖
重构方法:
// ❌ 不好:A和B循环依赖
A → B
↑ ↓
←───┘
// ✅ 好:提取公共逻辑到C
A → C ← B
3. 使用事件解耦
// 替代直接调用
@Component
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void createOrder(Order order) {
// 保存订单
orderRepository.save(order);
// 发送事件,不直接调用其他服务
eventPublisher.publishEvent(new OrderCreatedEvent(order));
}
}
@Component
public class InventoryService {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
// 减库存
}
}
十一、源码跟踪 🔍
核心方法
// DefaultSingletonBeanRegistry.getSingleton()
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
// 1. 先从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 2. 标记正在创建
beforeSingletonCreation(beanName);
try {
// 3. 调用工厂方法创建Bean
singletonObject = singletonFactory.getObject();
} finally {
// 4. 移除创建标记
afterSingletonCreation(beanName);
}
// 5. 放入一级缓存
addSingleton(beanName, singletonObject);
}
return singletonObject;
}
}
// 获取早期引用
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 2. 从二级缓存获取
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3. 从三级缓存获取工厂
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 4. 调用工厂方法获取早期引用
singletonObject = singletonFactory.getObject();
// 5. 放入二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 6. 从三级缓存移除
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
十二、总结口诀 📝
循环依赖要记清,
三级缓存来帮忙。
一级成品二级半,
三级工厂藏玄机。
字段注入能解决,
构造注入会翻车。
Prototype不缓存,
循环依赖解不了。
AOP需要三级存,
代理对象统一管。
设计合理是王道,
循环依赖要避免!
参考资料 📚
下期预告: 135-Spring的@Lookup方法注入的实现原理 🔍
编写时间:2025年
作者:技术文档小助手 ✍️
版本:v1.0
愿你的代码没有循环,只有优雅的单向流动! 🎯