spring循环依赖本质是bean初始化时互相等待,仅支持单例+字段/setter注入;构造器注入、原型bean等因无中间态或不缓存半成品而必然报beancurrentlyincreationexception。

Spring 中的循环依赖,本质是两个或多个 Bean 在初始化过程中互相等待对方先完成,比如 A 要注入 B,B 又要注入 A。它不是“不能发生”,而是要看注入方式和作用域——Spring 默认只对单例 + 字段或 setter 注入提供自动支持,其余场景大概率报 BeanCurrentlyInCreationException。
哪些情况会触发循环依赖?
常见源头包括:
- 领域模型双向关联(如 Order ↔ User,各自持有对方引用)
- Service 层拆分过细,公共逻辑未抽离,导致 A 和 B 直接互调
- 工具类之间为复用方法而相互依赖(例如 DateHelper ←→ JsonHelper)
- 升级 Spring Boot 2.6+ 后,默认禁止循环依赖,哪怕能自动解决也会启动失败
Spring 是怎么“救场”的?三级缓存原理
Spring 不靠魔法,靠分阶段暴露对象:
- 三级缓存(singletonFactories):存的是 ObjectFactory,能按需生成原始对象或代理(关键!支持 AOP)
- 二级缓存(earlySingletonObjects):存提前暴露的“半成品”实例(已实例化、未填充属性)
- 一级缓存(singletonObjects):存完全初始化好的最终 Bean
当 A 创建中需要 B,B 创建中又需要 A,Spring 就从三级缓存拿到 A 的工厂,生成早期引用塞进二级缓存,让 B 先用着——等 B 初始化完,再回过头补全 A。这个过程只对单例 + 非构造器注入有效。
构造器注入为什么一定失败?
因为构造器执行时,对象还没造出来,依赖必须“此时此刻”就到位。而循环中双方都卡在构造阶段,谁也给不出对方要的东西。没有缓存能绕过这一步。
- 字段注入:实例化后才赋值,留出了“提前暴露”的窗口
- 构造器注入:实例化 = 依赖注入,没有中间态,无法插手
实用解决方案选型
按优先级推荐:
- 重构设计(首选):把共用逻辑提到新类(如 CommonService),A 和 B 都只依赖它;或改用事件机制(ApplicationEventPublisher + @EventListener),实现异步解耦
-
@Lazy 懒加载(快速生效):仅在构造器参数上加
@Lazy,Spring 会注入一个临时代理,首次调用时才真正初始化目标 Bean -
改用字段或 setter 注入:放弃构造器强制依赖,接受 Spring 自动填充,但要注意 NPE 风险(可用
@NonNull+ 构造器校验辅助) -
手动获取 Bean(慎用):实现 ApplicationContextAware,在需要时通过
context.getBean()拿,破坏了依赖注入的透明性,仅限极特殊场景











