背景与问题
Spring Boot 项目里,BeanCurrentlyInCreationException、UnsatisfiedDependencyException、BeanCreationException 这几类异常,几乎是很多 Java 开发者都绕不过去的坑。
我自己第一次遇到时,表面上只是“应用启动失败”,实际上背后可能混着几层问题:
- 循环依赖:A 依赖 B,B 又依赖 A
- 初始化时机错误:Bean 还没准备好,就被拿去用了
- 构造器注入链过深:Spring 无法提前暴露对象
- 在
@PostConstruct、初始化方法里提前调用依赖 - 配置类、代理类、AOP 增强后,依赖关系和你以为的不一样
尤其从 Spring Boot 2.6 开始,默认就不再鼓励循环依赖,很多过去“能跑”的项目,升级后直接在启动阶段炸出来。
本文不讲空泛理论,我会按**“先复现 -> 再定位 -> 最后修复”**的方式,带你把这个坑真正走一遍。
一个典型报错长这样
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderService':
Unsatisfied dependency expressed through field 'paymentService';
nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'paymentService': Requested bean is currently in creation:
Is there an unresolvable circular reference?
看到这类报错,很多人第一反应是:
“不是都加了
@Service和@Autowired了吗?为什么 Spring 还会起不来?”
问题恰恰出在这里:加了注解不代表依赖关系合理。
现象复现
先做一个最小可运行示例,故意制造一个循环依赖。
示例结构
OrderService依赖PaymentServicePaymentService又依赖OrderService
如果使用构造器注入,这个问题最容易直接暴露。
实战代码(可运行)
1)启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CircularDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CircularDemoApplication.class, args);
}
}
2)OrderService
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public String createOrder() {
return "order created -> " + paymentService.pay();
}
}
3)PaymentService
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final OrderService orderService;
public PaymentService(OrderService orderService) {
this.orderService = orderService;
}
public String pay() {
return "payment success";
}
}
4)Controller
package com.example.demo.controller;
import com.example.demo.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/order")
public String order() {
return orderService.createOrder();
}
}
启动后,大概率会直接报循环依赖异常。
核心原理
要真正修好这个问题,不能只会“加个 @Lazy 试试看”。先把 Spring 的 Bean 创建机制理解到位,排查会快很多。
1. 什么是循环依赖
最简单的模型就是:
flowchart LR
A[OrderService] --> B[PaymentService]
B --> A
Spring 创建 OrderService 时,需要先拿到 PaymentService;
而创建 PaymentService 时,又反过来需要 OrderService。
如果两边都必须“现在立刻拿到一个完整对象”,就卡死了。
2. Spring 为什么有时能解决,有时不能
很多文章会说“Spring 可以解决循环依赖”,这句话不完整。
更准确地说:
- 单例 Bean
- 非构造器强依赖
- 允许提前暴露引用
- 没有过于复杂的代理与初始化逻辑
在这些条件下,Spring 有机会通过三级缓存来兜底一部分循环依赖。
Bean 创建的简化过程
flowchart TD
A[实例化 Bean] --> B[放入三级缓存 ObjectFactory]
B --> C[属性注入]
C --> D[初始化]
D --> E[放入一级缓存 singletonObjects]
如果另一个 Bean 在属性注入阶段需要它,Spring 可能会从“早期引用”里拿一个尚未完全初始化的对象先顶上。
但注意:
- 构造器注入时,对象连实例化后的“半成品”都还没法顺利流转
- 如果涉及
@Transactional、AOP 代理、@Async等,早期引用和最终代理对象可能不一致 - Spring Boot 新版本默认更严格,很多循环依赖直接拒绝
3. 为什么构造器注入更容易把问题暴露出来
这其实是件好事。
构造器注入要求:
- 依赖必须完整、明确
- 对象在创建时就处于可用状态
- 隐式依赖更难藏住
所以构造器注入虽然会让循环依赖“更早爆炸”,但它是在帮你发现设计问题。
相比之下,字段注入有时能“糊过去”,但问题会在运行期以更隐蔽的方式出现。
4. Bean 初始化异常不一定只是循环依赖
很多时候,表面是 BeanCreationException,根因却不止一个。常见链路如下:
sequenceDiagram
participant App as SpringBoot
participant Ctx as ApplicationContext
participant A as OrderService
participant B as PaymentService
App->>Ctx: refresh()
Ctx->>A: createBean(OrderService)
A->>Ctx: need PaymentService
Ctx->>B: createBean(PaymentService)
B->>Ctx: need OrderService
Ctx-->>B: BeanCurrentlyInCreationException
B-->>Ctx: BeanCreationException
Ctx-->>App: Application startup failed
这也是为什么你在日志里经常看到一长串异常嵌套。
真正要看的不是最外层,而是最深处的 root cause。
定位路径
实际排查时,我建议按下面这个顺序,不要一上来就全局搜索 @Autowired。
第一步:看最深层异常
重点找这些关键词:
BeanCurrentlyInCreationExceptionRequested bean is currently in creationUnsatisfiedDependencyExceptionInvocation of init method failedError creating bean with name
如果日志很多,可以直接搜:
currently in creation
或者:
nested exception
第二步:画出依赖链
比如报错里出现:
orderServicepaymentServicecouponService
那就很可能是:
flowchart LR
A[OrderService] --> B[PaymentService]
B --> C[CouponService]
C --> A
实际项目里,循环依赖未必是“两两互相依赖”,经常是三角依赖、配置类间接依赖、事件监听回调依赖。
第三步:确认依赖发生在哪个阶段
这是最容易被忽略的点。
依赖可能出现在:
- 构造器参数
- 字段注入 / Setter 注入
@PostConstructInitializingBean#afterPropertiesSet@Bean方法内部调用- 静态代码块 / 初始化表达式
- ApplicationRunner / CommandLineRunner 启动阶段
比如下面这种,就不是纯注入问题,而是初始化时机问题:
package com.example.demo.service;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
@Service
public class CacheWarmupService {
private final RemoteConfigService remoteConfigService;
public CacheWarmupService(RemoteConfigService remoteConfigService) {
this.remoteConfigService = remoteConfigService;
}
@PostConstruct
public void init() {
remoteConfigService.load();
}
}
如果 RemoteConfigService 初始化本身又依赖别的尚未就绪的 Bean,就会变成另一类 BeanCreationException。
修复方案
修复循环依赖,最重要的原则不是“让 Spring 能启动”,而是:
让依赖关系回到合理的方向。
下面按“推荐程度”排序。
方案一:重构职责,打断双向依赖
这是最推荐的方式。
错误设计
OrderService想调用支付PaymentService又回头操作订单状态- 两边彼此都知道对方
更合理的设计
引入一个中间协调者,例如 OrderPaymentFacade 或领域事件。
flowchart LR
A[OrderController] --> F[OrderPaymentFacade]
F --> B[OrderService]
F --> C[PaymentService]
改造后的代码
OrderService
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public String createOrderRecord() {
return "order created";
}
public String markPaid() {
return "order marked paid";
}
}
PaymentService
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
public String pay() {
return "payment success";
}
}
OrderPaymentFacade
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class OrderPaymentFacade {
private final OrderService orderService;
private final PaymentService paymentService;
public OrderPaymentFacade(OrderService orderService, PaymentService paymentService) {
this.orderService = orderService;
this.paymentService = paymentService;
}
public String createAndPay() {
String orderResult = orderService.createOrderRecord();
String payResult = paymentService.pay();
String statusResult = orderService.markPaid();
return orderResult + " | " + payResult + " | " + statusResult;
}
}
Controller
package com.example.demo.controller;
import com.example.demo.service.OrderPaymentFacade;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
private final OrderPaymentFacade facade;
public OrderController(OrderPaymentFacade facade) {
this.facade = facade;
}
@GetMapping("/order")
public String order() {
return facade.createAndPay();
}
}
这种改法的好处是:
- 消除双向依赖
- 业务边界更清晰
- 单元测试更好写
- 后续接 MQ、事件驱动也更自然
方案二:使用 @Lazy 作为止血方案
如果线上故障紧急、短期没法重构,可以先止血。
示例
package com.example.demo.service;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final OrderService orderService;
public PaymentService(@Lazy OrderService orderService) {
this.orderService = orderService;
}
public String pay() {
return "payment success";
}
}
这样 Spring 会注入一个延迟代理,而不是启动时立刻要求完整 Bean。
适用场景
- 紧急恢复启动
- 历史项目包袱较重
- 某些依赖确实只在少量路径中用到
边界条件
但我要强调,@Lazy 不是根治方案:
- 只是把“启动时失败”推迟到“运行时失败”
- 如果懒加载后第一次调用链仍然闭环,照样会出问题
- AOP、事务代理叠加时,调试复杂度会上升
方案三:改成事件驱动,解除直接引用
如果两个服务只是“一个动作完成后通知另一个”,那就不要互相注入。
发布事件
package com.example.demo.event;
public class OrderPaidEvent {
private final String orderId;
public OrderPaidEvent(String orderId) {
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
package com.example.demo.service;
import com.example.demo.event.OrderPaidEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final ApplicationEventPublisher publisher;
public PaymentService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public String pay(String orderId) {
publisher.publishEvent(new OrderPaidEvent(orderId));
return "payment success";
}
}
监听事件
package com.example.demo.listener;
import com.example.demo.event.OrderPaidEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class OrderPaidListener {
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("update order status: " + event.getOrderId());
}
}
这种方式特别适合:
- 订单支付后更新状态
- 发优惠券
- 记审计日志
- 推送消息通知
方案四:ObjectProvider / Provider 按需获取
如果确实需要偶发地拿某个 Bean,而不是强依赖,可以改成按需查找。
package com.example.demo.service;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final ObjectProvider<OrderService> orderServiceProvider;
public PaymentService(ObjectProvider<OrderService> orderServiceProvider) {
this.orderServiceProvider = orderServiceProvider;
}
public String pay() {
OrderService orderService = orderServiceProvider.getIfAvailable();
if (orderService != null) {
// 按需使用
}
return "payment success";
}
}
这个方案比字段注入 + 侥幸启动更清晰,但也要克制使用。
如果一个对象需要频繁 getBean,通常说明设计已经开始变味了。
常见坑与排查
这一节我专门列一些真实项目里高频但隐蔽的问题。
坑一:@PostConstruct 里做重操作
现象
应用启动慢,甚至启动失败。
典型问题代码
package com.example.demo.service;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
@Service
public class DictService {
private final RemoteApiClient remoteApiClient;
public DictService(RemoteApiClient remoteApiClient) {
this.remoteApiClient = remoteApiClient;
}
@PostConstruct
public void init() {
remoteApiClient.pullAll();
}
}
问题点
- 启动阶段就访问外部系统
- 外部依赖不稳定时直接拖垮容器初始化
- 容易和其他 Bean 的初始化链缠在一起
建议
- 把重操作迁移到
ApplicationReadyEvent - 做超时控制和失败降级
- 不要在 Bean 初始化阶段发起不可控的远程调用
坑二:@Configuration 类里相互调用 @Bean
现象
看起来不是 Service 循环依赖,但实际还是。
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AConfig {
@Bean
public AService aService(BService bService) {
return new AService(bService);
}
}
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BConfig {
@Bean
public BService bService(AService aService) {
return new BService(aService);
}
}
这本质上和 Service 双向依赖没有区别,只是藏在配置类里更难看出来。
坑三:事务代理导致“看起来没循环,实际有代理依赖”
例如:
AService标注了@TransactionalBService注入AServiceAService初始化时又依赖BService
这里容器里真正参与注入的,可能是代理对象而不是原始对象。
一旦早期引用和最终代理不一致,异常会更绕。
建议
- 不要把事务边界设计成互相回调
- 事务方法尽量放在清晰的应用服务层
- 避免一个事务服务反向依赖调用方
坑四:字段注入掩盖了设计问题
字段注入看起来很方便:
@Autowired
private PaymentService paymentService;
但它的问题是:
- 依赖不显式
- 单测不友好
- 容易形成“随手注入”的双向网状依赖
- 某些场景下问题不在编译期暴露
建议
优先用构造器注入。
哪怕它更容易在启动时报错,也比上线后在某个冷门路径炸掉强得多。
坑五:为了启动成功,开启循环依赖容忍
有些人会这么配:
spring:
main:
allow-circular-references: true
这确实可能让一部分旧项目“先跑起来”,但我不建议把它当常规方案。
为什么不推荐
- 掩盖架构问题
- 升级框架时风险更大
- 运行期行为更难预测
- 新人接手后会持续复制坏味道
什么时候可以临时用
- 老系统升级过渡期
- 必须先恢复服务
- 已经有明确的后续重构计划
如果要临时开,建议:
- 标注技术债
- 记录涉及的 Bean 链路
- 约定下个版本完成拆解
安全/性能最佳实践
循环依赖和初始化异常,表面是启动问题,往深了看,其实也会影响稳定性、安全性和性能。
1. 初始化阶段不要做外部调用洪峰
启动时如果几十个 Bean 都在 @PostConstruct 拉远程配置、预热缓存、建连接,很容易导致:
- 启动雪崩
- 外部依赖被瞬时打满
- 容器反复重启
建议
- 把预热逻辑迁移到应用就绪事件后
- 增加限流、超时、重试上限
- 关键初始化要可观测
2. 不要在初始化阶段处理敏感数据
比如:
- 启动时解密全部密钥
- 预加载所有用户令牌
- 初始化日志里打印配置对象
建议
- 敏感信息按需加载
- 日志脱敏
- 初始化失败日志中避免输出完整凭证、连接串、Token
3. 控制 Bean 的职责粒度
很多循环依赖的根因不是 Spring,而是类太胖。
如果一个 Service 同时负责:
- 业务编排
- 数据访问
- 缓存同步
- MQ 通知
- 审计日志
那它迟早会和一堆别的 Bean 缠在一起。
建议
按职责拆层:
Facade / ApplicationService:编排流程DomainService:核心业务逻辑Repository:持久化EventPublisher:事件发布Client:外部系统访问
4. 对初始化失败做快速失败与降级区分
不是所有初始化失败都应该阻止应用启动。
适合快速失败
- 数据源不可用
- 核心配置缺失
- 加密密钥加载失败
- 关键 Bean 创建失败
适合降级启动
- 非关键缓存预热失败
- 辅助字典加载失败
- 报表模块初始化失败
- 弱依赖外部接口不可用
这类边界划清楚,启动问题会少很多。
5. 给依赖图“瘦身”
如果项目越来越复杂,我建议定期做两件事:
- 查看模块依赖图
- 检查包之间是否出现双向引用
哪怕暂时不引入 ArchUnit 一类工具,至少也要在 code review 里盯住:
- controller 不要反向依赖 service 之外的实现细节
- service 之间尽量单向
- config 不要承载业务逻辑
一套实用排查清单
出问题时,可以直接照着过一遍。
启动失败时先看什么
- 是否包含
BeanCurrentlyInCreationException - 是否提示
Requested bean is currently in creation - 最深层 root cause 是哪个 Bean
- 哪几个 Bean 在异常链中反复出现
快速确认循环依赖
- A 是否依赖 B
- B 是否依赖 A
- 或 A -> B -> C -> A
判断是注入问题还是初始化时机问题
- 报错发生在构造器?
- 字段注入?
@PostConstruct?@Bean方法?ApplicationRunner?
决定修复策略
- 能否通过职责拆分消除双向依赖
- 是否适合引入 Facade
- 是否适合改事件驱动
- 是否只能短期
@Lazy止血 - 是否存在必须延期处理的历史债
一个更稳妥的修复思路
如果你在线上接到这个故障,我建议按这个顺序处理:
- 先定位具体依赖链
- 判断是否能快速重构打断
- 不能重构时用
@Lazy或ObjectProvider临时止血 - 回归测试事务、AOP、启动流程
- 补上重构任务,避免问题反复出现
很多团队的问题不是不会修,而是“为了尽快恢复,留下了更大的坑”。
这个问题最怕的就是:启动恢复了,但设计更乱了。
总结
Spring Boot 中的循环依赖与 Bean 初始化异常,本质上不是一个“注解没加对”的小问题,而是对象关系、初始化时机和职责边界共同作用的结果。
你可以记住这几个核心结论:
- 构造器注入更容易暴露真实问题,长期看是好事
- 循环依赖首选重构,而不是配置容忍
@Lazy、ObjectProvider适合止血,不适合长期滥用- 很多 Bean 初始化异常并非单纯循环依赖,而是初始化阶段做了不该做的事
- 把编排、业务、事件、外部访问拆开,依赖关系会自然清爽很多
如果只给一个最实用的建议,那就是:
当你看到两个 Service 互相注入时,先别急着让 Spring “兼容”,先问一句:这两个类是不是本来就不该互相知道对方?
很多坑,从这里开始就能少踩一半。