一句话先说结论:Bean 循环依赖就是 A 依赖 B、B 又依赖 A,Spring 创建时先给 bean 打上「正在创建」标记,回头再要同一个 bean 就撞上
BeanCurrentlyInCreationException。Spring Boot 2.6 起把spring.main.allow-circular-references默认值从true改成了false,所以老项目一升级就启动失败。最该做的是重构拆掉这个环;图快可以用@Lazy或换字段/setter 注入,临时兜底才去开那个开关。
背景
Spring 的 ApplicationContext 在启动阶段会按依赖关系把一个个 bean 实例化并组装起来。正常情况下,这张依赖图是一棵有向无环的树。循环依赖让图里出现了环,等于「先有鸡还是先有蛋」——容器没法把两边的对象都完整造出来。
最常见的现场是两个 @Service 互相注入:OrderService 要调用 ProductService 查商品,ProductService 又要调 OrderService 查订单。业务上看着合理,结构上却已经耦合过紧。
现象
升级到 2.6+,或者直接改成构造器注入,应用启动直接失败。控制台常见两种报法:
旧样式是底层异常:
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'beanA':
Requested bean is currently in creation: Is there an unresolvable circular reference?
2.6+ 的 FailureAnalysis 报得更直观,会直接把环画出来:
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| orderService defined in file [.../OrderService.class]
↑ ↓
| productService defined in file [.../ProductService.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default.
Update your application to remove the dependency cycle between beans.
As a last resort, it may be possible to break the cycle automatically by setting
spring.main.allow-circular-references to true.
能触发这套报错的复现代码长这样:
@Service
public class OrderService {
private final ProductService productService;
public OrderService(ProductService productService) {
this.productService = productService;
}
}
@Service
public class ProductService {
private final OrderService orderService;
public ProductService(OrderService orderService) {
this.orderService = orderService;
}
}
根因分析
Spring 创建单例 bean 时,第一步先把它放进 singletonsCurrentlyInCreation,标记「正在创建」。OrderService 的构造器需要 ProductService,容器就转去创建 ProductService;而 ProductService 的构造器又要 OrderService,容器一看「这个 bean 我正在建呢」,于是抛出 BeanCurrentlyInCreationException。
构造器注入之所以无解,是因为必须先拿到所有依赖才能调用构造器完成实例化,中间不存在「半成品」可以提前暴露。而字段/setter 注入则不同——Spring 框架本身有三级缓存(singletonFactories、earlySingletonObjects 这些),能在属性注入阶段把还没完全初始化的 bean 提前暴露出来,所以 2.5 之前默认就能「绕过去」。
真正的分水岭是 Spring Boot 2.6.0:它把 spring.main.allow-circular-references 的默认值从 true 改成了 false。也就是说,哪怕你用的是字段/setter 注入,2.6 之后也会在启动时被主动拦下来。这不是 bug,而是刻意为之——循环依赖往往是设计缺陷,fail fast 逼你去修,而不是让半初始化的 bean 在运行时埋雷。
排查时最有用的就是上面那段 cycle 图,它会列出环上每一个 bean。如果还看不清,就把 bean 的日志级别调成 DEBUG:
logging.level.org.springframework.beans=DEBUG
解决方案
首选:重构,拆掉环
从根本上让依赖单向化。最常见的做法是把双方共用的逻辑抽到第三个 bean 里:
@Service
public class PricingService {
public Money calc(Order order) { /* ... */ }
}
@Service
public class OrderService {
private final PricingService pricing;
public OrderService(PricingService pricing) { this.pricing = pricing; }
}
@Service
public class ProductService {
private final PricingService pricing;
public ProductService(PricingService pricing) { this.pricing = pricing; }
}
环消失了,两个 service 都只依赖 PricingService。这是最干净、最可持续的方案,也是官方推荐的方向。
@Lazy:打断即时初始化
在环的任意一边注入点加 @Lazy,Spring 会先注入一个代理,真正用到时才去初始化被依赖的 bean,从而打断启动时的死锁:
@Service
public class OrderService {
private final ProductService productService;
public OrderService(@Lazy ProductService productService) {
this.productService = productService;
}
}
构造器、字段、setter 三种注入都适用。代价是这只是绕过,不是消除,代码里依然存在强耦合,而且延迟初始化可能把错误推迟到运行时才暴露。
字段 / setter 注入 + 打开开关(临时兜底)
如果 2.6 之前用字段注入一直没问题、升级后才踩雷,短期内可以在配置里放开:
spring:
main:
allow-circular-references: true
开关打开后,字段或 setter 注入能借助三级缓存正常构建,但构造器注入依旧不行。这个开关在官方文档里被明确标为 last resort,属于迁移过渡的临时手段,不建议长期依赖——你等于保留下了一个设计缺陷,而且它可能在未来的版本里被彻底移除。
小结
- 这个报错的本质是依赖图成环,不是 Spring 抽风。
- 升级到 2.6+ 最容易撞上,因为默认禁止循环依赖了。
- 优先重构拆环;图快用
@Lazy;别图省事长期开着allow-circular-references。 - 排查先从 cycle 图入手,必要时打开
logging.level.org.springframework.beans=DEBUG。
来源
- Resolve Circular Dependency in Spring Boot — LightTrace
- Fix: Spring Boot “The dependencies of some of the beans in the application context form a cycle” — FixDevs
- Fix: Spring BeanCreationException — FixDevs
- SpringBoot 2.6.x 默认禁用循环依赖后的应对策略 — springcloud.io
- spring.main.allow-circular-references — Spring Boot 配置说明