Spring Boot 循环依赖解决方案完全指南

一、什么是循环依赖?

简单来说,循环依赖是指两个或多个 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 代理) |

解决流程

  1. 创建 ServiceA → 将原始对象工厂放入三级缓存
  2. 填充 ServiceA 属性 → 发现需要 ServiceB
  3. 创建 ServiceB → 将原始对象工厂放入三级缓存
  4. 填充 ServiceB 属性 → 从三级缓存获取 ServiceA 的工厂 → 生成代理对象
  5. ServiceB 初始化完成 → 移入一级缓存
  6. 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 提供了多种变通方案,但重构代码消除循环依赖才是最佳实践。

核心原则

  1. 构造器注入作为默认选择(暴露真实依赖)
  2. 遇到循环依赖先考虑重构而非绕过
  3. 必须使用 @Lazy 时做好代码注释,标记技术债务
  4. 多模块项目中通过提取公共模块打破循环
相关推荐
Scott9999HH15 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔15 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社16 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海16 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖17 小时前
JDK 26 新特性详解
java·开发语言
马优晨18 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区19 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大20 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai20 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说20 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json