spring boot 本身不直接解决循环依赖,真正起作用的是 spring framework 的 ioc 容器三级缓存机制;它仅在单例作用域、非全构造器注入、无 aop 代理冲突前提下,通过 singletonfactories→earlysingletonobjects→singletonobjects 的分阶段暴露与填充,实现字段/setter 循环依赖的破局。

Spring Boot 本身不直接解决循环依赖,真正起作用的是底层 Spring Framework 的 IoC 容器机制。它通过三级缓存配合 Bean 生命周期的分阶段管理,在单例(Singleton)+ 字段或 setter 注入的前提下,巧妙“破局”循环依赖。但必须清楚:这不是万能方案,构造器注入、原型(Prototype)Bean 或多作用域混合场景下,它无能为力。
哪些循环依赖能被解决?关键前提有三个
不是所有循环依赖 Spring 都能处理,只有同时满足以下条件才可解:
- Bean 必须是单例作用域(Singleton):只有单例 Bean 才会被放入三级缓存进行统一管理;原型 Bean 每次创建都是新实例,无法复用中间状态。
- 不能全部使用构造器注入:构造器注入要求依赖对象在实例化前就必须就位,而双方都卡在这一步,形成死锁;字段(@Autowired)或 setter 注入允许先造出空壳对象,再填属性。
- 不能涉及 AOP 代理的时机冲突:如果两个 Bean 都需要代理(如加了 @Transactional),Spring 会在三级缓存中通过 ObjectFactory 动态决定是否返回原始对象还是代理对象——这步很关键,但若代理逻辑过于复杂(如多个切面嵌套),仍可能失败。
三级缓存怎么协作?每级干啥、何时用
三级缓存不是并列关系,而是按 Bean 创建流程严格递进的“状态暂存区”,核心目标是:在属性填充阶段,让正在创建中的 Bean 能被其他 Bean 提前引用。
-
三级缓存(singletonFactories):存的是 ObjectFactory 工厂对象,例如
() -> new ServiceA()。Bean 实例化后立即注册进来,是“最早暴露”的入口,但此时对象还没设属性、没走初始化方法,甚至可能没代理。 - 二级缓存(earlySingletonObjects):存的是从三级缓存工厂里取出来的“早期引用”(early reference)。当另一个 Bean 需要它时,Spring 会调用工厂的 getObject(),得到一个原始或代理后的半成品对象,并把它放进二级缓存,同时从三级缓存移除该工厂——避免重复创建。
- 一级缓存(singletonObjects):存的是完全体 Bean:属性已注入、初始化方法执行完、代理也织入完毕。Bean 创建流程走完后,从二级缓存移出,正式入驻这里,后续所有请求都从此取。
一次典型流程:A 依赖 B,B 又依赖 A
以 OrderService 和 UserService 相互字段注入为例,Spring 启动时实际发生如下步骤:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 开始创建
orderService→ 调用构造器生成空实例 → 立即注册其 ObjectFactory 到三级缓存 - 进入属性注入阶段 → 发现需
userService→ 暂停orderService,转去创建userService - 创建
userService→ 构造完成 → 注册其 ObjectFactory 到三级缓存 -
userService开始注入属性 → 发现需orderService→ 查一级缓存(空)、查二级缓存(空)、查三级缓存(命中) - 调用
orderService的 ObjectFactory.getObject() → 若需代理则生成代理对象,否则返回原始实例 → 放入二级缓存,清除三级缓存中的 factory -
userService完成注入和初始化 → 放入一级缓存 - 回到
orderService继续注入 → 从一级缓存拿到完整的userService→ 自身完成初始化 → 入驻一级缓存
Spring Boot 2.6+ 默认禁用,怎么开?
从 Spring Boot 2.6 开始,默认禁止循环依赖,启动时直接报错。若确认业务合理且无法重构,可在 application.properties 中显式开启:
spring.main.allow-circular-references=true
注意:YAML 格式写法为:
spring:<br> main:<br> allow-circular-references: true
这个开关只影响容器是否抛异常,并不改变三级缓存的工作机制——机制一直存在,只是默认被“刹车”了。










