Spring 循环依赖深度解析:三级缓存为什么不是两级?构造器注入和属性注入如何选择?
本文从实际项目问题出发,深入讲解 Spring 循环依赖的产生原因、三级缓存设计思想、为什么不能使用两级缓存,以及构造器注入和属性注入的区别。最后结合微服务架构分析循环依赖问题及最佳实践。
一、前言
最近在项目启动时遇到了这样一个异常:
BeanCurrentlyInCreationException:
Bean with name 'thTakeoffMsgHandler' has been injected into other beans
[thPlanMsgHandle] in its raw version as part of a circular reference,
but has eventually been wrapped.
很多开发者看到这个异常第一反应都是:
Spring不是有三级缓存吗?为什么还会循环依赖?
事实上,这个问题涉及 Spring IOC、AOP 代理以及 Bean 生命周期,是 Spring 框架中最经典的话题之一。
本文将从源码设计思想出发,彻底弄懂 Spring 为什么设计三级缓存,以及在实际开发中如何避免循环依赖。
二、什么是循环依赖?
循环依赖(Circular Dependency)就是两个或多个 Bean 相互依赖。
例如:
A
│
▼
B
│
▼
A
代码如下:
@Service
public class AService {
@Autowired
private BService bService;
}
@Service
public class BService {
@Autowired
private AService aService;
}
Spring 在创建 A 时,需要先创建 B。
而创建 B 时,又需要 A。
于是形成死循环。
三、Spring Bean 创建流程
Spring 创建 Bean 可以简化为下面几个步骤:
实例化对象(new)
↓
属性注入(@Autowired)
↓
Aware接口回调
↓
BeanPostProcessor
↓
初始化(@PostConstruct)
↓
AOP代理
↓
放入一级缓存
注意:
对象实例化和属性注入并不是同一个阶段。
也正因为实例化先于依赖注入,Spring 才有机会解决一部分循环依赖。
四、Spring 为什么能解决循环依赖?
假设:
A
↓
B
↓
A
创建过程:
第一步
Spring:
new A()
此时:
A 已经存在。
但是:
@Autowired
@PostConstruct
初始化
都还没执行。
第二步
A 需要 B。
Spring 去创建 B。
第三步
B 又需要 A。
Spring 发现:
A 已经实例化了。
于是:
把 A 提前暴露出去。
B 就能继续创建。
最后:
B 创建完成
↓
A 注入 B
↓
A 创建完成
整个流程结束。
五、Spring 的三级缓存
Spring 内部维护三个缓存。
一级缓存
singletonObjects
作用:
存放已经完全初始化完成的 Bean。
singletonObjects
A(完成)
B(完成)
所有 Bean 创建完成后都会进入这里。
二级缓存
earlySingletonObjects
作用:
存放提前暴露的 Bean。
例如:
new A()
虽然:
@Autowired
@PostConstruct
初始化
还没有执行。
但是:
可以先给其它 Bean 使用。
三级缓存
singletonFactories
注意:
三级缓存里面放的不是 Bean。
而是:
ObjectFactory
也就是:
Bean工厂
里面保存的是:
() -> getEarlyBeanReference(beanName)
它的作用就是:
需要的时候,再决定返回普通对象还是代理对象。
六、为什么不能只使用两级缓存?
这是 Spring 面试最经典的问题。
答案:
因为 Spring 需要支持 AOP。
假设:
@Transactional
public class OrderService {
}
真正放入 Spring 容器里的其实不是:
OrderService
而是:
TransactionProxy(OrderService)
也就是代理对象。
如果只有两级缓存:
一级缓存
二级缓存
那么:
创建 Bean 时:
new OrderService()
只能提前暴露:
原始对象
其它 Bean 拿到的也是:
OrderService
而不是:
TransactionProxy
那么:
事务就失效了。
因为:
其它 Bean 调用的是原始对象。
不是代理对象。
三级缓存解决了这个问题。
流程如下:
new OrderService()
↓
放入三级缓存(ObjectFactory)
↓
其它Bean需要OrderService
↓
调用ObjectFactory
↓
getEarlyBeanReference()
↓
如果需要代理
↓
提前生成代理对象
↓
放入二级缓存
↓
删除三级缓存
↓
其它Bean拿到代理对象
↓
最终一级缓存也是代理对象
这样:
整个 Spring 容器中:
所有 Bean 引用的都是同一个代理对象。
因此:
事务不会失效。
七、三级缓存为什么不能直接存代理对象?
很多人会继续问:
三级缓存直接放代理对象不就行了吗?
答案是不行。
原因有两个。
原因一:不是所有 Bean 都需要代理
例如:
UserService
没有:
@Transactional
@Async
@Cacheable
那么:
根本不用生成代理。
如果:
Spring 一开始全部生成代理。
会严重浪费性能。
原因二:代理对象创建成本较高
Spring 使用:
JDK动态代理
CGLIB代理
都需要:
解析注解
匹配Advisor
生成代理类
创建代理对象
这些工作都比较耗时。
所以:
Spring 采用:
懒生成
只有真正需要的时候:
才调用:
ObjectFactory
生成代理对象。
八、构造器注入与属性注入有什么区别?
Spring 常见注入方式:
属性注入
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
特点:
对象先创建。
然后:
Spring 再注入依赖。
构造器注入
@Service
public class UserService {
private final OrderService orderService;
public UserService(OrderService orderService){
this.orderService = orderService;
}
}
特点:
创建对象之前:
依赖必须已经存在。
否则:
无法创建对象。
九、为什么构造器注入不能解决循环依赖?
来看流程。
A:
需要B
B:
需要A
如果:
构造器注入:
new A(B)
但是:
B 还没有。
于是:
Spring 去创建 B。
new B(A)
结果:
A 也没有。
因为:
A 连:
new
都没完成。
三级缓存根本没有机会保存 A。
于是:
直接抛:
BeanCurrentlyInCreationException
因此:
构造器注入无法利用三级缓存解决循环依赖。
十、属性注入为什么可以?
属性注入流程:
new A()
↓
放入三级缓存
↓
创建B
↓
B引用A
↓
完成B
↓
回到A
↓
注入B
↓
初始化完成
因为:
A 已经实例化。
Spring 才能提前暴露。
所以:
三级缓存才能发挥作用。
十一、微服务中的循环依赖
很多人认为:
微服务之间也会出现 Spring 循环依赖。
其实这是两个完全不同的问题。
例如:
ServiceA
↓
Feign
↓
ServiceB
这是:
HTTP调用【Feign/RestTemplate/Dubbo/gRPC】
不是:
Bean注入
所以:
不会出现:
BeanCurrentlyInCreationException
真正可能出现的是:
业务循环。
例如:
订单服务
↓
库存服务
↓
支付服务
↓
订单服务
这属于:
RPC循环调用
它会导致:
-
请求超时
-
死循环
-
服务雪崩
-
熔断
解决方式通常是:
-
MQ 异步解耦
-
事件驱动
-
Saga 分布式事务
-
TCC
-
Outbox Pattern
而不是 Spring 的三级缓存。
十二、实际开发推荐使用哪种注入方式?
很多开发规范(如 Spring 官方推荐)都更倾向于构造器注入,原因包括:
-
依赖关系清晰,创建对象时必须满足依赖。
-
可以使用
final字段,提高对象不可变性。 -
更方便单元测试,容易注入 Mock 对象。
-
能尽早暴露设计问题,例如循环依赖。
相比之下,属性注入虽然书写简单,但依赖关系不够显式,也更容易在大型项目中形成隐藏耦合。
需要注意的是:
不要因为构造器注入暴露了循环依赖,就改回属性注入。
真正应该做的是重新设计代码,拆分职责,而不是依赖三级缓存“帮忙解决”。
十三、如何避免循环依赖?
如果项目中出现循环依赖,建议优先考虑以下方案:
-
拆分公共逻辑:将双方共同依赖的功能抽取到新的 Service 中。
-
引入事件机制:使用 Spring Event 或 MQ,避免直接互相调用。
-
重新划分职责:一个类只负责一个业务方向,减少双向依赖。
-
必要时使用
@Lazy:可以作为临时方案,但不建议长期依赖。 -
避免过度耦合:如果两个 Service 长期互相调用,通常意味着设计需要调整。
| 对比项 | 构造器注入 | 字段/属性注入(@Autowired/@Resource) |
|---|---|---|
| 推荐程度 | 更推荐 | 兼容老项目较多 |
是否支持 final |
是 | 否 |
| 是否便于单元测试 | 是 | 一般 |
| 是否容易发现设计问题 | 是 | 相对不容易 |
| Spring 能否利用三级缓存尝试解决循环依赖 | 否 | 可以(但遇到代理 Bean 不一定成功) |
十四、总结
Spring 的三级缓存并不是为了“炫技”,而是为了在支持循环依赖的同时,保证 AOP 代理对象的一致性。
理解三级缓存的关键,可以归纳为三点:
-
一级缓存:保存完全初始化完成的单例 Bean。
-
二级缓存:保存提前暴露的 Bean(或提前暴露的代理对象)。
-
三级缓存:保存
ObjectFactory,用于按需创建早期引用,决定返回原始对象还是代理对象。
在工程实践中,更重要的是认识到:
循环依赖通常不是一个 Spring 技术问题,而是一个软件设计问题。
三级缓存只是 Spring 提供的一种兼容机制,而不是鼓励开发者编写相互耦合的代码。真正优秀的系统,应通过合理的职责划分、单向依赖和事件驱动等方式,从设计层面避免循环依赖的产生。
更多推荐



所有评论(0)