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. 多模块项目中通过提取公共模块打破循环
相关推荐
爱丽_2 小时前
GC 怎么判定“该回收谁”:GC Roots、可达性分析、四种引用与回收算法
java·jvm·算法
bbq粉刷匠2 小时前
Java--多线程--单例模式
java·开发语言·单例模式
随风,奔跑2 小时前
Spring MVC
java·后端·spring
dfafadfadfafa2 小时前
嵌入式C++安全编码
开发语言·c++·算法
计算机安禾2 小时前
【C语言程序设计】第34篇:文件的概念与文件指针
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
追风林2 小时前
idea支持本地 的 服务器 远程debug
java·服务器·intellij-idea
凸头2 小时前
AI 流式聊天接口实现(WebFlux+SSE)
java·人工智能
简宸~2 小时前
VS Code + LaTex + SumatraPDF联合使用指南
java·vscode·latex·sumatrapdf
弦有三种苦难2 小时前
CCF-202412-T3缓存模拟90分
java·开发语言·spring