SpringBoot2.6+单例bean字段注入的循环依赖问题

TL;DR

看面经的时候顺手测了一下Spring的三级缓存,发现默认配置是无法解决单例Bean通过Setter/字段注入形成的循环依赖问题。研究了一下之后发现:SpringL3Cache对于解决单例模式下setter或字段注入的CycleReference并不总是默认开启的,SpringFrameWork默认开启,但在SpringBoot2.6+的版本中(本地SpringBoot3.0.5)就不默认容许循环依赖,具体来说是allow-circular-references=false,不允许提前暴露对象到L2~L3Cache中。

虽然可以通过spring.main.allow-circular-references: true来开启,但这也说明SpringBoot官方建议:如果条件允许,coding时尽量不要出现循环依赖。这也是Idea建议使用构造器注入的原因,提前暴露可能的循环依赖问题。

可见循环依赖确实人厌狗嫌。

默认禁用提前暴露三缓。

具体的

Spring的三级缓存机制具备解决部分循环依赖的能力,但Spring Boot 2.6+默认禁止Bean之间的循环依赖。

Spring能处理的典型情况是:单例Bean之间,通过Setter / 字段注入形成的循环依赖。其核心是在Bean完成初始化之前,通过提前暴露 Bean 引用来打破依赖环。


SpringBoot2.6默认不允许提前暴露引用

从Spring Boot 2.6开始,spring.main.allow-circular-references 默认值改为false,因此即使底层仍然存在三级缓存,Spring Boot默认也不会允许通过“提前暴露 Bean”来解决循环依赖。

执行问题,报错:默认SpringBoot2.6+ this.allowCircularReferences 为 false 时:创建 Bean 时不允许提前暴露 singleton 到L2~L3Cache。
1. A 实例化后不会往 singletonFactories(三级缓存)里放 ObjectFactory;
2. B 注入 A 时调 getSingleton("test05_A", true) → L1/L2/L3 全查不到 → 返回 null;
3. 于是去真正 doGetBean("test05_A") → 而 A 已在 singletonsCurrentlyInCreation 里 → beforeSingletonCreation 直接抛 BeanCurrentlyInCreationException。

如果确实需要,可以配置:

spring:
  main:
    allow-circular-references: true

重新允许Spring尝试解决循环依赖。官方同时明确建议不要依赖循环引用,而应该从代码设计上消除依赖环。


构造器注入

构造器注入并不能解决循环依赖,反而会让循环依赖直接暴露出来。例如A(B b)和B(A a):创建A必须先有B,创建B又必须先有 A,此时 A 连“原始对象”都还没有实例化出来,自然无法提前暴露。所以构造器循环依赖通常直接启动失败。这也是推荐构造器注入的一个好处:让不合理的依赖关系更早暴露,而不是由三级缓存悄悄兜底。

IDEA 推荐构造器注入的原因之一,是构造器注入能够让 Bean 之间的依赖关系更加明确,并且让循环依赖在创建阶段直接暴露,而不是依靠 Spring 的提前引用机制进行兜底;它是帮助发现并避免循环依赖,并非解决循环依赖。


Spring和SpringBoot的默认行为不同,但都不建议使用循环依赖。

Spring Framework 自身的 AbstractAutowireCapableBeanFactory#allowCircularReferences 默认实际上仍然是 true,而 Spring Boot 2.6+ 在 Boot 层把它配置成了 false。Spring Framework 默认值 和 Spring Boot 默认行为不同。Spring Framework 的 Javadoc 也明确说循环引用会导致某个 Bean 拿到“尚未完全初始化”的另一个 Bean,因此官方建议不要依赖这种机制。