一、什么是循环依赖?
简单来说,循环依赖是指两个或多个 Bean 相互依赖,形成一个闭环。例如:ServiceA 依赖 ServiceB,而 ServiceB 又依赖 ServiceA。
// 典型的循环依赖场景
@Service
public class OrderService {
@Autowired
private UserService userService; // OrderService -> UserService
}
@Service
public class UserService {
@Autowired
private OrderService orderService; // UserService -> OrderService (形成闭环)
}
**Tip:**Spring Boot 2.6+ 版本默认禁止循环依赖,启动时会抛出异常 。
二、Spring 的三级缓存机制
Spring 通过三级缓存 解决 Setter/Field 注入的循环依赖,但无法解决构造器注入的循环依赖 :
|----------|-------------------------|-------------------------------|
| 缓存级别 | 名称 | 存储内容 |
| 一级缓存 | singletonObjects | 完全初始化的 Bean |
| 二级缓存 | earlySingletonObjects | 提前暴露的早期 Bean(未完成属性填充) |
| 三级缓存 | singletonFactories | Bean 工厂对象(用于生成早期引用,支持 AOP 代理) |
解决流程:
- 创建 ServiceA → 将原始对象工厂放入三级缓存
- 填充 ServiceA 属性 → 发现需要 ServiceB
- 创建 ServiceB → 将原始对象工厂放入三级缓存
- 填充 ServiceB 属性 → 从三级缓存获取 ServiceA 的工厂 → 生成代理对象
- ServiceB 初始化完成 → 移入一级缓存
- ServiceA 继续填充属性 → 从一级缓存获取 ServiceB → 完成初始化
三、六大解决方案(附代码实战)
方案 1:重构代码设计(推荐)
根本解决之道。循环依赖通常是设计问题的信号,应重新审视职责划分。
重构策略:
-
提取公共逻辑到第三个服务
-
使用接口隔离原则拆分大接口
-
应用单一职责原则
// ❌ 重构前:OrderService <-> CustomerService 循环依赖
// ✅ 重构后:提取 CustomerOrderService 作为中介
@Service
public class CustomerOrderService {
@Autowired
private CustomerService customerService;
@Autowired
private OrderService orderService;public List<Order> getCustomerOrders(Long customerId) { Customer customer = customerService.getCustomer(customerId); return orderService.getOrdersByCustomer(customerId); }}
方案 2:使用 @Lazy 延迟加载(最优雅)
在循环依赖的一侧添加 @Lazy 注解,强制延迟初始化。Spring 会注入一个代理对象,首次调用时才真正初始化 。
@Service
public class OrderService {
private final UserService userService;
// 构造器注入 + @Lazy
public OrderService(@Lazy UserService userService) {
this.userService = userService;
}
}
@Service
public class UserService {
private final OrderService orderService;
// 正常注入
public UserService(OrderService orderService) {
this.orderService = orderService;
}
}
原理 :@Lazy 引入了一个第三方代理 Bean,打破了循环依赖,形成延迟加载效果。真正加载依赖 Bean 是在首次调用方法时 。
字段注入方式:
@Service
public class OrderService {
@Lazy
@Autowired
private UserService userService;
}
方案 3:Setter/Field 注入替代构造器注入
Spring 可以处理 Setter 注入和字段注入的循环依赖,但无法处理构造器注入的循环依赖 。
// Setter 注入方式
@Service
public class OrderService {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
// 或字段注入(简洁但不推荐用于生产代码)
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
方案 4:使用 ObjectProvider/ObjectFactory
通过 Spring 的 ObjectProvider 实现延迟解析,在需要时才获取 Bean 。
@Service
public class OrderService {
private final ObjectProvider<UserService> userServiceProvider;
public OrderService(ObjectProvider<UserService> userServiceProvider) {
this.userServiceProvider = userServiceProvider;
}
public void processOrder(Long userId) {
// 仅在需要时才解析依赖
UserService userService = userServiceProvider.getObject();
User user = userService.getUser(userId);
// ...
}
}
方案 5:事件驱动解耦
使用 Spring 事件机制彻底解耦强依赖关系 。
// 定义事件
public class OrderCreatedEvent {
private final Long orderId;
private final Long userId;
// constructor & getters
}
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher publisher;
public Order createOrder(Long userId, List<Item> items) {
Order order = orderRepository.save(new Order(userId, items));
// 发布事件代替直接调用
publisher.publishEvent(new OrderCreatedEvent(order.getId(), userId));
return order;
}
}
@Service
public class UserService {
// 不再直接依赖 OrderService!
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
User user = userRepository.findById(event.getUserId()).orElseThrow();
user.incrementOrderCount();
userRepository.save(user);
}
}
方案 6:配置允许循环依赖(⚠️ 临时方案)
在 application.properties 中配置允许循环引用,仅用于遗留项目迁移:
spring.main.allow-circular-references=true
或在 YAML 中:
spring:
main:
allow-circular-references: true
注意 :Spring Boot 2.6+ 默认值为 false,生产环境不建议开启 。
四、方案对比与选型建议
方案推荐度从高到底
|--------------------|-------------|-------------|------------------|
| 方案 | 适用场景 | 优点 | 缺点 |
| 重构设计 | 新项目或允许重构的场景 | 根本解决问题,架构清晰 | 需要较多工作量 |
| @Lazy | 快速修复,单向依赖 | 简单,保持构造器注入 | 隐藏设计问题,首次调用有性能损耗 |
| 事件驱动 | 需要彻底解耦的场景 | 完全解耦,支持异步 | 调试复杂,事务处理需注意 |
| Setter 注入 | 遗留代码快速修复 | 兼容性好 | 依赖关系不明确,可测试性降低 |
| ObjectProvider | 动态解析场景 | 灵活,延迟加载 | 代码冗余 |
| 配置允许 | 临时过渡方案 | 最快解决 | 掩盖问题,技术债务 |
五、最佳实践与黄金法则
1. 优先使用构造器注入
构造器注入强制暴露所有依赖,使循环依赖在编译期就能被发现 。
2. 定期架构审查
使用 ArchUnit 等工具检测循环依赖:
@ArchTest
static final ArchRule noCircularDependencies =
slices().matching("com.example.(*)..")
.should().beFreeOfCycles();
3. 紧急修复流程
发现循环依赖 → 使用 @Lazy 临时解决 → 标记技术债务 → 排期重构
4. 决策流程图
发现循环依赖
↓
能否重构? ──是──→ 提取公共逻辑到新服务 ──→ 问题解决
↓ 否
是设计问题? ──是──→ 使用事件驱动解耦 ──→ 问题解决
↓ 否
一次性初始化? ──是──→ 使用 @Lazy 注解 ──→ 问题解决
↓ 否
使用 ObjectProvider 延迟解析 ──→ 问题解决
六、总结
循环依赖是代码异味(Code Smell) ,通常表明组件职责划分不清晰。虽然 Spring 提供了多种变通方案,但重构代码消除循环依赖才是最佳实践。
核心原则:
- 构造器注入作为默认选择(暴露真实依赖)
- 遇到循环依赖先考虑重构而非绕过
- 必须使用
@Lazy时做好代码注释,标记技术债务 - 多模块项目中通过提取公共模块打破循环