一、什么是循环依赖?为什么是个问题?
假设你有两个 Service:
java
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
@Service
public class OrderService {
@Autowired
private UserService userService;
}
Spring 创建 Bean 的逻辑是:
要创建
UserService→ 发现它需要OrderService→ 先去创建OrderService→ 发现OrderService需要UserService→ 但UserService还没创建完...
这就是一个死循环。如果没有机制打破它,Spring 启动时就会卡死,最后栈溢出。
二、构造器注入为什么解决不了循环依赖?
这是很多人踩坑的地方。如果你用构造器注入:
java
@Service
public class UserService {
private final OrderService orderService;
public UserService(OrderService orderService) {
this.orderService = orderService;
}
}
@Service
public class OrderService {
private final UserService userService;
public OrderService(UserService userService) {
this.userService = userService;
}
}
为什么这里 Spring 无法解决循环依赖?
因为,构造器注入的时机是在实例化阶段:
创建 UserService → 调用构造方法 → 构造方法需要 OrderService →
创建 OrderService → 调用构造方法 → 构造方法需要 UserService →
创建 UserService → 调用构造方法 → ...
实例化(调用构造方法)是创建 Bean 的第一步。 在这一步就需要依赖对象,但那个依赖对象也还没实例化。Spring 没有任何"半成品"可以给你,因为它连对象都还没 new 出来。
所以构造器注入的循环依赖,Spring 直接报错,启动失败。
三、Spring 解决循环依赖的核心思路:提前暴露半成品
如果你用 Setter 注入 或 字段注入 (@Autowired 直接写在字段上),Spring 就能解决。
为什么?因为,这两个方式的依赖注入发生在"实例化之后"。
Spring 的设计逻辑是:
先把 Bean 的"空壳"(只调了构造方法,属性还没填)提前暴露出来,让别的 Bean 先引用着。等所有 Bean 的"空壳"都创建好了,再回头一个个填属性。
这就像工厂流水线:
-
先批量把所有零件的外壳做出来。
-
再统一往里面装配件。
-
这样即使 A 的外壳需要装 B,B 的外壳需要装 A,也不用担心------因为两个外壳都已经存在了。
四、三级缓存:Spring 是怎么实现"提前暴露"的?
Spring 内部用了三级缓存 来管理这个过程。三级缓存就是三个 Map,分别存不同状态的 Bean:
| 缓存级别 | 名称 | 存放的内容 |
|---|---|---|
| 一级缓存 | singletonObjects |
完全初始化好的 Bean(成品) |
| 二级缓存 | earlySingletonObjects |
半成品 Bean(实例化了,但属性还没注入) |
| 三级缓存 | singletonFactories |
Bean 的工厂对象(用来生成半成品) |
为什么要分三级? 因为 Spring 需要区分"这个 Bean 到底走到哪一步了",避免把没准备好的 Bean 当成成品给出去。
完整流程(以 UserService 和 OrderService 为例)
1. Spring 开始创建 UserService
↓
2. 调用 UserService 的构造方法,new 出一个对象(此时 orderService 字段是 null)
↓
3. 把这个"半成品" UserService 包装成一个工厂,放入【三级缓存 singletonFactories】
(相当于告诉容器:"UserService 的壳已经做好了,谁需要可以来拿")
↓
4. UserService 需要注入 OrderService,Spring 转去创建 OrderService
↓
5. 调用 OrderService 的构造方法,new 出一个对象(此时 userService 字段是 null)
↓
6. 把半成品 OrderService 放入【三级缓存】
↓
7. OrderService 需要注入 UserService
↓
8. Spring 去缓存里找 UserService:
- 一级缓存(成品)?没有,UserService 还没初始化完。
- 二级缓存(半成品)?没有。
- 三级缓存(工厂)?有!
↓
9. 从三级缓存的工厂里取出 UserService 的半成品,放入【二级缓存 earlySingletonObjects】
然后把这个半成品注入到 OrderService 中
↓
10. OrderService 的属性注入完成,继续初始化(@PostConstruct 等)
↓
11. OrderService 完全初始化好,放入【一级缓存 singletonObjects】
并从三级缓存中移除
↓
12. 回到 UserService,现在 OrderService 已经在一级缓存里了(是成品)
把成品 OrderService 注入到 UserService 中
↓
13. UserService 初始化完成,放入【一级缓存】
循环就这样被打破了。 关键点是第 3 步------在属性注入之前,就把半成品暴露到三级缓存里。这样当 OrderService 回头找 UserService 时,虽然 UserService 还没准备好,但至少能拿到一个"壳",不至于死循环。
五、二级缓存和三级缓存的区别
你可能想问:为什么需要三级缓存?二级不行吗?
只用两级缓存(一级 + 二级)理论上也能解决循环依赖。 但 Spring 加了第三级,是为了处理 AOP 代理。
如果 Bean 需要被 AOP 代理(比如加了 @Transactional),Spring 不能直接把原始对象暴露出去------因为最终注入的应该是代理对象。
三级缓存里存的是 ObjectFactory(工厂),当需要提前暴露 Bean 时,工厂会判断:
"这个 Bean 要不要创建代理?如果要,我提前把代理对象生成出来给你。"
这样即使 Bean 还在创建中,别的 Bean 拿到的也是正确的代理对象,而不是原始对象。
如果没有三级缓存,只有二级缓存:
-
只能存放原始对象。
-
如果 Bean 需要代理,提前暴露的就是原始对象。
-
最后注入到其他 Bean 里的是原始对象,而不是代理对象。
-
导致 AOP 失效,事务不生效。
六、Spring 解决不了的循环依赖
以下三种情况,Spring 无法自动解决:
1. 构造器注入的循环依赖
前面讲过了,实例化阶段就需要依赖,没有半成品可以暴露。
解决方案:改用 Setter/字段注入。
2. 原型(Prototype)作用域的循环依赖
java
@Scope("prototype")
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
原型 Bean 每次请求都创建新的实例,Spring 不会缓存它们。没有缓存,就没有"提前暴露"的机制。
解决方案:改用单例(默认就是单例),或者重构代码消除循环依赖。
3. 使用 @Async 或 @DependsOn 产生的循环
@DependsOn 强制指定了创建顺序,如果形成循环,Spring 无法打破。
七、企业级推荐做法
方案 1:重构代码,消除循环依赖(最推荐)
循环依赖通常是设计问题。比如:
java
// 循环依赖:UserService ↔ OrderService
UserService 需要 OrderService 来查订单
OrderService 需要 UserService 来查用户
解决方案:引入中间层
java
@Service
public class UserQueryService {
// 只负责查用户,不依赖 OrderService
}
@Service
public class OrderQueryService {
// 只负责查订单,不依赖 UserService
}
@Service
public class UserOrderFacade {
// 组合查询,依赖 UserQueryService 和 OrderQueryService
@Autowired
private UserQueryService userQueryService;
@Autowired
private OrderQueryService orderQueryService;
}
把双向依赖拆成单向依赖,或者引入一个上层门面(Facade)。
方案 2:使用 @Lazy 延迟加载
如果确实无法避免循环,可以用 @Lazy:
java
@Service
public class UserService {
@Lazy
@Autowired
private OrderService orderService;
}
原理: @Lazy 不会立刻注入真实的 Bean,而是注入一个代理对象。当真正调用 orderService 的方法时,代理才会去容器里拿真正的 Bean。
这时候循环链被延迟到了运行时,而不是启动时。
方案 3:用 Setter 注入代替构造器注入
如果必须用构造器注入(比如为了不可变性),但又遇到循环依赖,可以折中:
java
@Service
public class UserService {
private OrderService orderService;
// 构造器注入其他必须的依赖
public UserService(SomeOtherService other) {
this.someOther = other;
}
// 用 Setter 注入可能循环的依赖
@Autowired
public void setOrderService(OrderService orderService) {
this.orderService = orderService;
}
}
八、总结
| 问题 | 答案 |
|---|---|
| Spring 为什么能解决循环依赖? | 因为它在属性注入之前,就把 Bean 的半成品放入三级缓存,让别的 Bean 可以先引用。 |
| 为什么构造器注入不行? | 构造器注入在实例化阶段就需要依赖,此时连半成品都没有,无法提前暴露。 |
| 三级缓存的作用? | 一级存成品,二级存半成品,三级存工厂(用于提前生成代理对象)。 |
| @Lazy 怎么解决的? | 注入的是代理对象,真实 Bean 的获取延迟到方法调用时,避开了启动期的循环。 |
| 最佳实践? | 优先重构代码消除循环依赖;无法避免时用 @Lazy 或 Setter 注入。 |