spring如何解决循环依赖,以及哪些场景下会失效?

Spring 仅能解决 "单例 Bean + setter 注入/字段注入" 场景的循环依赖,核心方案是通过 "三级缓存机制" 提前暴露"半成品 Bean",打破依赖循环,具体流程和原理如下:

一、核心前提:仅支持特定场景

Spring 无法解决所有循环依赖,仅针对满足以下条件的场景生效:

Bean 作用域:必须是 单例(多例 Bean 不存入缓存,无法提前暴露)。

注入方式:必须是 setter 注入 或 字段注入(构造器注入卡在实例化阶段,无法提前暴露半成品)。

二、核心方案:三级缓存的协作

Spring 通过三个缓存(Map 结构)分层存储 Bean 的不同状态,实现"提前暴露半成品 → 注入依赖 → 完成初始化"的闭环,打破循环。

  1. 三级缓存的职责
缓存级别 缓存名称(源码中) 存储内容 核心作用
第一级缓存 singletonObjects 完全初始化完成的单例 Bean 供容器后续直接获取已就绪的 Bean,避免重复初始化,是最终的"成品缓存"。
第二级缓存 earlySingletonObjects 提前暴露的半成品单例 Bean(已实例化,但未执行 populateBean 注入属性、未执行 initializeBean 初始化方法) 当循环依赖触发时,直接从该缓存获取"半成品 Bean",避免重复执行"获取半成品"的逻辑(如动态代理创建),提升效率。
第三级缓存 singletonFactories 创建半成品 Bean 的工厂对象(ObjectFactory 接口实现) 1. 当单例 Bean 完成实例化(调用构造器)后,立即将其封装为 ObjectFactory 存入该缓存,实现"提前暴露"的第一步;2. 若 Bean 需要动态代理(如 @Transactional 标记的 Bean),工厂会在首次被调用时(循环依赖触发时)创建代理对象,确保后续注入的是代理实例,而非原始 Bean。

三、解决循环依赖的完整流程(示例:A 依赖 B,B 依赖 A)

流程图

以"单例 Bean A(setter 注入 B)"和"单例 Bean B(setter 注入 A)"为例,流程如下:

1.初始化 Bean A:

css 复制代码
执行 A 的构造器,完成 实例化(此时 A 是半成品,无属性 B);

将 A 封装为 ObjectFactory,存入 3级缓存(singletonFactories),完成"提前暴露"的第一步;

开始为 A 注入属性 B,触发 Bean B 的初始化。

2.初始化 Bean B:

css 复制代码
执行 B 的构造器,完成 实例化(此时 B 是半成品,无属性 A);

将 B 封装为 ObjectFactory,存入 3级缓存;

开始为 B 注入属性 A,触发 A 的获取。

3.获取半成品 A 注入 B:

css 复制代码
先查 1级缓存:A 未完成初始化,无;

再查 2级缓存:A 未被暴露到这里,无;

最后查 3级缓存:找到 A 的 ObjectFactory,调用工厂方法生成 A 的半成品(若 A 需要动态代理,此时创建代理对象);

将 A 的半成品存入 2级缓存,并从 3级缓存删除(避免重复生成);

将 A 半成品注入 B,B 完成属性注入和后续初始化,成为 成品,存入 1级缓存。

4.完成 Bean A 的初始化:

css 复制代码
从 1级缓存 获取已成品的 B,注入 A;

A 完成属性注入和后续初始化,成为 成品,存入 1级缓存,并从 2级缓存删除。
    

四、 失效场景

Spring 无法解决循环依赖的核心场景,均源于Bean 初始化流程中无法通过"提前暴露半成品 Bean"机制打破依赖循环,主要集中在以下三类情况:

一、构造器注入导致的循环依赖

这是最典型且 Spring 明确无法解决的场景。

当两个或多个 Bean 通过构造器参数相互依赖时(如 A 的构造器需要 B,B 的构造器需要 A),Spring 初始化 Bean 时需先实例化构造器参数(依赖 Bean),但此时依赖 Bean 同样需要先实例化当前 Bean,形成"实例化阶段"的死循环,无法提前暴露半成品 Bean(半成品 Bean 需先完成实例化,而构造器注入卡在实例化第一步)。

typescript 复制代码
示例(无法解决)
@Component
public class A {
    // A 的构造器依赖 B
    public A(B b) {}
}

@Component
public class B {
    // B 的构造器依赖 A
    public B(A a) {}
}

二、多例(Prototype)Bean 的循环依赖

无论是通过构造器、setter 还是字段注入,只要循环依赖的 Bean 中存在多例(@Scope("prototype"))Bean,Spring 均无法解决。

原因是多例 Bean 不支持"提前暴露半成品 Bean":Spring 对多例 Bean 的策略是"每次获取都重新创建实例",不会将其存入容器的"三级缓存"(用于提前暴露半成品 Bean 的缓存),导致依赖循环时无法获取到半成品实例,只能反复触发新实例创建,最终抛出循环依赖异常。

less 复制代码
示例(无法解决)
@Component
@Scope("prototype") // 多例 Bean
public class A {
    @Autowired
    private B b; // setter/字段注入 B
}

@Component
@Scope("prototype") // 多例 Bean
public class B {
    @Autowired
    private A a; // setter/字段注入 A
}

三、BeanPostProcessor 或 BeanFactoryPostProcessor 参与的循环依赖

若循环依赖的 Bean 中,有一个是 BeanPostProcessor(或其实现类,如 AutowiredAnnotationBeanPostProcessor),或依赖链中包含 BeanFactoryPostProcessor,Spring 可能无法解决。 原因是 BeanPostProcessor 的初始化优先级极高(早于普通 Bean),其依赖的 Bean 会被提前触发初始化,但此时普通 Bean 的"提前暴露"机制尚未完全生效,或 BeanPostProcessor 自身的处理逻辑会中断半成品 Bean 的暴露流程,导致循环依赖无法打破。

less 复制代码
示例(可能无法解决)
// 自定义 BeanPostProcessor,依赖普通 Bean B
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
    @Autowired
    private B b; // 依赖 B
    // ... 实现方法
}

// 普通 Bean B 依赖 MyBeanPostProcessor(或其依赖的 Bean)
@Component
public class B {
    @Autowired
    private A a; // 若 A 又依赖 MyBeanPostProcessor,形成循环
}

总结

Spring 仅能解决**"单例 Bean + setter/字段注入"** 场景的循环依赖(依赖"三级缓存"提前暴露半成品 Bean);上述三类场景因突破了"提前暴露"的前提(构造器卡实例化、多例不存缓存、处理器优先级冲突),均无法解决。

五、关键结论

Spring 解决循环依赖的本质是:在 Bean 实例化后、初始化前,通过三级缓存提前暴露"半成品 Bean",让依赖方先拿到可用的实例,待自身初始化完成后再反哺给被依赖方,最终打破循环。 该机制仅适用于"单例 + setter/字段注入",构造器注入多例 Bean单例的代理bean通过setter注入设置了@DependsOn注解的bean 等场景因无法提前暴露半成品,均无法解决。

相关推荐
IT_陈寒1 小时前
Python开发者必知的5个高效技巧,让你的代码速度提升50%!
前端·人工智能·后端
yvya_2 小时前
Mybatis总结
java·spring·mybatis
谦行2 小时前
Andrej Karpathy 谈持续探索最佳大语言模型辅助编程体验之路
后端
ALex_zry2 小时前
Golang云端编程入门指南:前沿框架与技术全景解析
开发语言·后端·golang
绝无仅有2 小时前
部署 Go 项目的 N 种方法
后端·面试·github
回家路上绕了弯2 小时前
Dubbo 实战指南:从架构原理到高可用落地,解锁分布式服务治理新能力
后端·dubbo
MaxHua2 小时前
SQL查询优化全指南:从语句到架构的系统性优化策略
后端·数据分析
用户4099322502123 小时前
如何在API高并发中玩转资源隔离与限流策略?
后端·ai编程·trae
似水流年流不尽思念3 小时前
Spring声明式事务原理及事务失效场景?
后端·面试