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

深入理解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.dtcms.com/a/314196.html

相关文章:

  • 鸿蒙南向开发 编写一个简单子系统
  • 机器学习——学习路线
  • MySQL进阶:(第八篇)深入解析InnoDB存储架构
  • 高效洗牌:Fisher-Yates算法详解
  • 软考 系统架构设计师系列知识点之杂项集萃(118)
  • 直播 app 系统架构分析
  • 如何在 Ubuntu 24.04 LTS 上安装 Docker
  • 计算机网络:
  • 团购商城 app 系统架构分析
  • (五)系统可靠性设计
  • android TextView lineHeight 是什么 ?
  • 国产化低代码平台如何筑牢企业数字化安全底座
  • 学习日志27 python
  • 远程机器操作--学习系列004
  • Vue Router快速入门
  • 数据从mysql迁移到postgresql
  • Petalinux快捷下载
  • 项目一:Python实现PDF增删改查编辑保存功能的全栈解决方案
  • WPF 按钮背景色渐变
  • LLM开发——基于Graph RAG知识图谱检索增强生成
  • steam Rust游戏 启动错误,删除sys驱动,亲测有效。
  • MySQL 约束知识体系:八大约束类型详细讲解
  • Spring Cloud Gateway 实现登录校验:构建统一认证入口
  • 网站从HTTP升级到HTTPS网址方法
  • AWS Lambda Function 全解:无服务器计算
  • 力扣top100--哈希
  • AWS VPC Transit Gateway 可观测最佳实践
  • 【MySQL】配置复制拓扑
  • Qt 商业应用开发流程与规范
  • 【Pytorch✨】LSTM03 三大门