Spring 如何解决循环依赖
在开发过程中,有遇到过Spring循环依赖问题吗?怎么解决的?
什么是 Spring 循环依赖?
简单来说,Spring 循环依赖 就是两个或多个 Bean 之间互相持有对方的引用,形成闭环依赖关系。比如:
-
A Bean 的创建需要依赖 B Bean,而 B Bean 的创建又需要依赖 A Bean;
-
更复杂的情况:A → B → C → A 这样的环形依赖。
举个直观的例子:
|
|
当 Spring 容器初始化时,创建 A 会先去创建 B,创建 B 又要先创建 A,形成 “死循环”,如果没有 Spring 的特殊处理,就会抛出 BeanCurrentlyInCreationException 异常。
实际开发中遇到的循环依赖场景
我在实际项目(包括基于若依框架的开发)中多次遇到循环依赖,常见场景有:
-
业务层互相依赖:比如用户服务(UserService)依赖订单服务(OrderService),订单服务又需要查询用户信息,反向依赖 UserService;
-
控制器与服务层循环依赖:若依框架中,某个 Controller 注入了 Service,而该 Service 又通过某些方式(如事件监听、工具类)反向注入了 Controller;
-
跨模块依赖:微服务拆分不彻底时,模块 A 的 Bean 依赖模块 B 的 Bean,模块 B 又依赖模块 A 的 Bean;
-
自定义 BeanPostProcessor 引发的循环依赖:自定义后置处理器时注入了业务 Bean,而业务 Bean 又依赖后置处理器。
Spring 对循环依赖的默认处理机制
首先要明确:Spring 仅能解决「单例 Bean」的「setter 注入 / 字段注入」的循环依赖,其他情况无法自动解决。
Spring 解决循环依赖的核心是「三级缓存」:
-
一级缓存(singletonObjects):存放完全初始化完成的单例 Bean;
-
二级缓存(earlySingletonObjects):存放提前曝光的、未完全初始化的 Bean(仅实例化,未填充属性和执行初始化方法);
-
三级缓存(singletonFactories):存放 Bean 的工厂对象,用于生成提前曝光的代理对象。
核心流程(以 A ↔ B 为例):
- 创建 A Bean:实例化 A → 将 A 的工厂放入三级缓存 → 填充属性(需要 B);
- 创建 B Bean:实例化 B → 将 B 的工厂放入三级缓存 → 填充属性(需要 A);
- B 从三级缓存获取 A 的提前曝光对象 → 填充到 B 中 → B 初始化完成,放入一级缓存;
- A 获取到初始化完成的 B → 填充到 A 中 → A 初始化完成,放入一级缓存。
循环依赖的解决方案(分场景)
场景 1:Spring 能自动解决的情况(单例 + setter / 字段注入)
无需手动处理,Spring 三级缓存会自动解决,这也是若依框架中最常见的情况。
场景 2:Spring 无法自动解决的情况(需要手动处理)
方案 1:使用 @Lazy 延迟加载(推荐) 通过延迟加载打破循环依赖的闭环,在注入时不立即创建依赖 Bean,而是创建代理对象,直到首次使用时才初始化。
|
|
方案 2:改用构造器注入 + @Autowired (required = false)
构造器注入默认不支持循环依赖,但可以通过设置非必须注入来规避:
|
|
方案 3:通过 ApplicationContext 获取 Bean(兜底方案)
放弃自动注入,手动从容器中获取依赖 Bean,打破直接依赖:
|
|
方案 4:重构代码(根本解决方案)
循环依赖往往是代码设计不合理的体现,比如:
提取公共逻辑到新的服务类(如把 A 和 B 都依赖的逻辑抽成 C 服务,A 和 B 都依赖 C,而非互相依赖);
若依框架中,可将 Service 之间的循环依赖拆分为「基础服务 + 业务服务」,基础服务不依赖其他业务服务。
方案 5:设置 Bean 为原型(prototype)
原型 Bean 每次获取都会新建,不会存入三级缓存,因此不会有循环依赖问题,但会增加内存开销,仅适用于非单例场景:
|
|
五、若依框架中处理循环依赖的实战示例 若依框架中最常见的是 Service 层循环依赖,比如 SysUserService 依赖 SysRoleService,SysRoleService 又依赖 SysUserService,解决方案如下:
|
|
总结
核心定义:Spring 循环依赖是 Bean 之间互相引用形成闭环,Spring 仅自动解决「单例 + setter / 字段注入」的循环依赖;
常见场景:业务层互相依赖、跨模块依赖,若依框架中 Service 层循环依赖最典型;
解决方案:优先用 @Lazy 延迟加载,其次重构代码优化设计,兜底方案是手动从 ApplicationContext 获取 Bean。