当前位置: 首页 > backend >正文

深入理解Spring中的循环依赖及解决方案

深入理解Spring中的循环依赖及解决方案

在Spring框架的日常使用中,"循环依赖"是一个高频出现且容易让人困惑的问题。新手往往在遇到BeanCurrentlyInCreationException时手足无措,而即使是有经验的开发者,也可能对Spring解决循环依赖的底层逻辑一知半解。本文将从概念入手,深入剖析循环依赖的产生原因、Spring的处理机制,以及实战中的解决方案。

一、什么是循环依赖?

循环依赖,顾名思义,是指两个或多个Bean之间互相依赖,形成一个闭环的依赖关系。

1.1 直观案例

最典型的循环依赖是"双向依赖",例如:
双向依赖
此时在创建A对象的同时需要使用的B对象,在创建B对象的同时需要使用到A对象,AB形成了A→B→A的闭环,导致无限等待,这就是最基础的循环依赖。

1.2 复杂场景

循环依赖也可能是多节点的闭环,例如A→B→C→A,或者更复杂的网状依赖(如A依赖B和C,B依赖C,C依赖A)。无论结构如何,核心都是"依赖关系形成了无法直接按顺序初始化的闭环"。
复杂循环依赖

二、循环依赖为什么会成为问题

在讨论Spring的处理逻辑前,我们需要先理解:为什么循环依赖会导致问题?

这要从Bean的初始化流程(Bean的生命周期)说起。Spring创建Bean的核心步骤是:

  1. 实例化:通过构造器创建Bean的对象(new操作);
  2. 属性注入:为Bean的依赖属性赋值(如@Autowired标注的字段);
  3. 初始化:执行@PostConstruct方法、实现InitializingBean接口的afterPropertiesSet方法等。

正常情况下,Bean的创建是"线性"的:先创建依赖的Bean,再创建当前Bean。例如A依赖B时,Spring会先创建B,再创建A并注入B。

但循环依赖打破了这种线性关系。以A→B→A为例:

  • 要创建A,需要先创建B;
  • 要创建B,又需要先创建A;
  • 陷入"先有鸡还是先有蛋"的死循环。

循环依赖

三、Spring如何处理循环依赖?

Spring并非对所有循环依赖都束手无策。事实上,对于单例Bean的字段注入(或Setter注入),Spring能自动解决循环依赖,这得益于它的"三级缓存"机制。

3.1 三级缓存的核心设计

Spring通过三个缓存(称为"三级缓存")来协调单例Bean的创建与依赖注入,这三个缓存定义在DefaultSingletonBeanRegistry中:
三级缓存

缓存名称作用级别
singletonObjects存储完全初始化完成的单例Bean(key:Bean名称,value:Bean实例)一级
earlySingletonObjects存储提前暴露的未完全初始化的单例Bean(仅实例化未注入属性)二级
singletonFactories存储Bean工厂(用于提前暴露未初始化的Bean,避免重复创建)三级

3.2 三级缓存解决循环依赖的流程

解决一般对象的循环依赖

A→B→A的双向依赖为例,我们一步步拆解Spring的处理逻辑:
解决一般对象的循环依赖

  1. 创建A的流程

    • Spring尝试获取A,发现三个缓存中都没有;
    • 开始创建A:先实例化A(执行构造器,得到"半成品"A,仅分配了内存,未注入属性);
    • 将A的“半成品”对象放入二级缓存earlySingletonObjects
    • 准备为A注入属性,发现依赖B,于是暂停A的创建,转去创建B。
  2. 创建B的流程

    • Spring尝试获取B,三个缓存中都没有;
    • 实例化B(得到"半成品"B);
    • 将B的“半成品”放入二级缓存earlySingletonObjects
    • 准备为B注入属性,发现依赖A,转去获取A。
  3. 解决A的依赖

    • 尝试获取A时,发现二级缓存earlySingletonObjects中有A的实例对象;
    • 取出A的"半成品",将"半成品"A注入到B中,B的属性注入完成;
    • B完成初始化,放入一级缓存singletonObjects,并从二级缓存中移除B。
  4. 完成A的创建

    • 回到A的属性注入步骤,此时B已在一级缓存中,直接将B注入A;
    • A完成初始化,放入一级缓存singletonObjects,并从二级缓存中移除A。

最终,A和B都成为"完全体",存储在一级缓存中,循环依赖被解决。

解决代理对象的循环依赖

A→B→A的双向依赖为例,我们一步步拆解Spring的处理逻辑:
解决代理对象的循环依赖问题

  1. 创建A的流程

    • Spring尝试获取A,发现三个缓存中都没有;
    • 开始创建A:先实例化A(执行构造器,得到"半成品"A,仅分配了内存,未注入属性);
    • 将A的工厂(singletonFactory)放入三级缓存singletonFactories
    • 准备为A注入属性,发现依赖B,于是暂停A的创建,转去创建B。
  2. 创建B的流程

    • Spring尝试获取B,三个缓存中都没有;
    • 实例化B(得到"半成品"B);
    • 将B的工厂放入三级缓存singletonFactories
    • 准备为B注入属性,发现依赖A,转去获取A。
  3. 解决A的依赖

    • 尝试获取A时,发现三级缓存singletonFactories中有A的工厂;
    • 通过工厂取出A的"半成品",放入二级缓存earlySingletonObjects,并从三级缓存中移除A的工厂;
    • 将"半成品"A注入到B中,B的属性注入完成;
    • B完成初始化,放入一级缓存singletonObjects,并从二级缓存中移除B。
  4. 完成A的创建

    • 回到A的属性注入步骤,此时B已在一级缓存中,直接将B注入A;
    • A完成初始化,放入一级缓存singletonObjects,并从二级缓存中移除A。

最终,A和B都成为"完全体",存储在一级缓存中,循环依赖被解决。

四、Spring无法解决的循环依赖场景

并非所有循环依赖都能被Spring自动处理。以下两种场景会导致BeanCurrentlyInCreationException

4.1 构造器注入的循环依赖

如果循环依赖通过构造器注入,Spring无法解决。例如:

@Service
public class AService {private BService bService;// 构造器注入B@Autowiredpublic AService(BService bService) {this.bService = bService;}
}@Service
public class BService {private AService aService;// 构造器注入A@Autowiredpublic BService(AService aService) {this.aService = aService;}
}

原因:构造器注入要求"先获取依赖才能实例化当前Bean"。创建A时需要先创建B,创建B时又需要先创建A,而此时两者都未实例化,无法提前暴露到缓存中,导致死循环。

4.2 多例(Prototype)Bean的循环依赖

Spring默认仅处理单例(Singleton) Bean的循环依赖。对于多例(@Scope("prototype"))Bean,循环依赖会直接报错。

原因:多例Bean每次获取都会创建新实例,不会存入三级缓存(缓存仅用于单例)。因此,创建A时需要B,创建B时又需要新的A,无限创建新对象导致溢出。

五、循环依赖的解决方案

针对不同场景的循环依赖,我们可以采用以下解决方案:

5.1 解决构造器注入的循环依赖

方案1:使用@Lazy延迟初始化

@Lazy注解可以让依赖的Bean延迟到第一次使用时才初始化,而非在当前Bean创建时立即初始化。例如:

@Service
public class AService {private BService bService;@Autowiredpublic AService(@Lazy BService bService) { // 对B延迟初始化this.bService = bService;}
}

此时,Spring会为B创建一个代理对象注入A,当A第一次使用B时才会真正创建B实例,打破了初始化时的闭环。

方案2:改用字段注入或Setter注入

将构造器注入改为字段注入(@Autowired标注字段)或Setter注入,利用Spring对单例字段注入的自动处理机制:

@Service
public class AService {private BService bService;// Setter注入@Autowiredpublic void setBService(BService bService) {this.bService = bService;}
}

5.2 解决多例Bean的循环依赖

多例Bean的循环依赖无法通过缓存解决,需从设计上规避:

方案1:将多例Bean改为单例

如果业务允许,将@Scope("prototype")改为默认的单例,利用三级缓存自动处理。

方案2:通过工厂手动获取

在多例Bean中,不直接注入依赖,而是通过ApplicationContextObjectFactory动态获取,避免初始化时的依赖:

@Service
@Scope("prototype")
public class AService {@Autowiredprivate ObjectFactory<BService> bFactory; // 工厂public void doSomething() {BService b = bFactory.getObject(); // 需要时才获取// ...}
}

5.3 通用方案:重构代码,消除循环依赖

循环依赖往往是代码设计不合理的信号(例如职责划分不清晰)。最根本的解决方案是重构:

  • 提取公共依赖:将A和B共同依赖的逻辑抽离为新的Bean(如CService),让A和B都依赖C,而非互相依赖;
  • 引入中间层:通过中介者模式(Mediator)减少Bean之间的直接依赖;
  • 拆分Bean职责:如果一个Bean承担过多职责,可能导致与多个Bean产生依赖,拆分后可减少依赖关系。

六、总结

Spring的循环依赖处理是其Bean管理机制的重要组成部分,核心依赖三级缓存实现单例字段注入的自动处理。但对于构造器注入和多例Bean,仍需手动干预。

在实际开发中,建议:

  1. 优先通过重构消除循环依赖,这是最健康的方式;
  2. 必须保留循环依赖时,单例Bean优先用字段注入,构造器注入可配合@Lazy
  3. 多例Bean尽量避免循环依赖,必要时通过工厂动态获取。

理解循环依赖的本质和Spring的处理逻辑,不仅能解决实际问题,更能帮助我们设计出更清晰、低耦合的代码结构。

http://www.xdnf.cn/news/16964.html

相关文章:

  • ssh连接VirtualBox中的Ubuntu24.04(win11、putty、NAT 模式)
  • 模型蒸馏(Distillation):原理、算法、应用
  • 每日任务day0804:小小勇者成长记之药剂师的小咪
  • 深入剖析Java Stream API性能优化实践指南
  • AgxOrin平台JetPack5.x版本fix multi-cam race condition 补丁
  • (2023ICML)BLIP-2:使用冻结图像编码器和大语言模型引导语言-图像预训练
  • Ubuntu共享文件夹权限设置
  • 【数据结构初阶】--顺序表(一)
  • 使用AWS for PHP SDK实现Minio文件上传
  • nodejs 封装方法将打印日志输出到指定文件
  • mybatis-plus报错Caused by: java.sql.SQLException: 无效的列类型: 1111
  • 论文Review LIO Multi-session Voxel-SLAM | 港大MARS出品!体素+平面特征的激光SLAM!经典必读!
  • Spring Boot 应用结合 Knife4j 进行 API 分组授权管理配置
  • 【世纪龙科技】汽车自动变速器拆装虚拟实训软件
  • 国产化低代码平台如何筑牢企业数字化安全底座
  • Go语言 并发安全sync
  • Linux 磁盘管理
  • 如何选择一个容易被搜索引擎发现的域名?
  • 从零开始的云计算生活——项目实战
  • Perl 面向对象编程深入解析
  • 京东商品销量数据如何获取?API接口调用操作详解
  • AWS VPC Transit Gateway 可观测最佳实践
  • [特殊字符] Ubuntu 下 MySQL 离线部署教学(含手动步骤与一键脚本)
  • Java 课程,每天解读一个简单Java之正整数分解质因数
  • 在安卓中使用 FFmpegKit 剪切视频并添加文字水印
  • 05.Redis 图形工具RDM
  • io_getevents 和 io_pgetevents 系统调用及示例
  • 论文阅读:DMD蒸馏 | One-step Diffusion with Distribution Matching Distillation
  • SaProt 模型部署与运行教程
  • 【AI】文档理解