Spring Boot 循环依赖问题解决方案笔记(基于电商系统示例)

1. 问题背景

以一个电商系统为例子,Spring Boot 应用启动时抛出了循环依赖(Circular Dependency)异常,错误信息如下:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

   orderController
┌─────┐
|  orderServiceImpl
↑     ↓
|  userServiceImpl
└─────┘

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.

1.1 问题描述

  • 循环依赖路径
    • OrderController 依赖 OrderServiceImpl
    • OrderServiceImpl 依赖 UserServiceImpl
    • UserServiceImpl 依赖 OrderServiceImpl
    • 形成闭环:OrderServiceImplUserServiceImplOrderServiceImpl
  • Spring 的行为
    • Spring 在创建 Bean 时需要解析依赖。
    • 由于循环依赖,Spring 无法完成 Bean 的创建,抛出异常。
  • Spring Boot 版本影响
    • Spring Boot 2.6 之前,默认允许循环依赖(通过代理模式解决)。
    • Spring Boot 2.6 及之后,默认禁止循环依赖(spring.main.allow-circular-references 默认为 false)。

2. 问题分析

2.1 依赖关系分析

  • OrderController
    • 注入 OrderService
      @RestController
      public class OrderController {
          @Autowired
          private OrderService orderService;
      }
      
  • OrderServiceImpl
    • 依赖 UserService(用于获取用户信息):
      @Service
      public class OrderServiceImpl implements OrderService {
      
          @Autowired
          private OrderMapper orderMapper;
      
          @Autowired
          private UserService userService;
      
          @Override
          public OrderVO createOrder(OrderVO orderVO) {
              // 获取用户信息
              UserEntity user = userService.findById(orderVO.getUserId());
              if (user == null) {
                  throw new RuntimeException("用户不存在");
              }
      
              OrderEntity order = new OrderEntity();
              BeanUtils.copyProperties(orderVO, order);
              order.setUserName(user.getName());
              orderMapper.insert(order);
      
              return orderVO;
          }
      }
      
  • UserServiceImpl
    • 依赖 OrderService(用于发送通知时查询用户最近的订单信息):
      @Service
      public class UserServiceImpl implements UserService {
      
          @Autowired
          private UserMapper userMapper;
      
          @Autowired
          private OrderService orderService;
      
          @Override
          public UserEntity findById(String userId) {
              UserEntity user = userMapper.selectById(userId);
              if (user != null) {
                  // 查询用户最近的订单并发送通知
                  OrderEntity latestOrder = orderService.findLatestOrderByUserId(userId);
                  if (latestOrder != null) {
                      System.out.println("用户 " + user.getName() + " 的最近订单: " + latestOrder.getId());
                  }
              }
              return user;
          }
      
          // 模拟方法
          public void sendNotification(String userId, String message) {
              System.out.println("发送通知给用户 " + userId + ": " + message);
          }
      }
      
  • OrderMapper 和 UserMapper
    • 假设为 MyBatis-Plus 的 Mapper 接口:
      @Mapper
      public interface OrderMapper extends BaseMapper<OrderEntity> {
          OrderEntity findLatestOrderByUserId(@Param("userId") String userId);
      }
      
      @Mapper
      public interface UserMapper extends BaseMapper<UserEntity> {
      }
      

2.2 循环依赖的成因

  • OrderServiceImpl 需要 UserService 来获取用户信息(userService.findById)。
  • UserServiceImpl 需要 OrderService 来查询用户最近的订单(orderService.findLatestOrderByUserId),以便发送通知。
  • 这种双向依赖形成了循环。

3. 解决方案

Spring 建议通过重构代码消除循环依赖,而不是依赖于允许循环依赖的配置。以下是几种解决方案:

3.1 方案 1:重构依赖关系(推荐)

  • 目标
    • 打破 OrderServiceImplUserServiceImpl 之间的循环依赖。
  • 方法
    • 移除 UserServiceImplOrderServiceImpl 的依赖。
    • 直接注入 OrderMapper,通过 Mapper 查询订单信息。
  • 实现
    • 修改 UserServiceImpl
      @Service
      public class UserServiceImpl implements UserService {
      
          @Autowired
          private UserMapper userMapper;
      
          @Autowired
          private OrderMapper orderMapper; // 直接注入 Mapper
      
          @Override
          public UserEntity findById(String userId) {
              UserEntity user = userMapper.selectById(userId);
              if (user != null) {
                  // 直接通过 Mapper 查询用户最近的订单
                  OrderEntity latestOrder = orderMapper.findLatestOrderByUserId(userId);
                  if (latestOrder != null) {
                      System.out.println("用户 " + user.getName() + " 的最近订单: " + latestOrder.getId());
                  }
              }
              return user;
          }
      
          public void sendNotification(String userId, String message) {
              System.out.println("发送通知给用户 " + userId + ": " + message);
          }
      }
      
  • 优点
    • 符合依赖倒挂原则(Service 层不直接依赖其他 Service,而是通过 Mapper 访问数据)。
    • 简单直接,彻底消除循环依赖。
  • 缺点
    • 如果 OrderServiceImpl 中有复杂的业务逻辑(例如缓存、权限检查),直接使用 Mapper 会绕过这些逻辑。

3.2 方案 2:使用 @Lazy 注解

  • 目标
    • 通过延迟加载的方式,暂时解决循环依赖问题。
  • 方法
    • UserServiceImpl 中,将 OrderService 的注入标记为 @Lazy
  • 实现
    • 修改 UserServiceImpl
      @Service
      public class UserServiceImpl implements UserService {
      
          @Autowired
          private UserMapper userMapper;
      
          @Autowired
          @Lazy
          private OrderService orderService;
      
          @Override
          public UserEntity findById(String userId) {
              UserEntity user = userMapper.selectById(userId);
              if (user != null) {
                  OrderEntity latestOrder = orderService.findLatestOrderByUserId(userId);
                  if (latestOrder != null) {
                      System.out.println("用户 " + user.getName() + " 的最近订单: " + latestOrder.getId());
                  }
              }
              return user;
          }
      
          public void sendNotification(String userId, String message) {
              System.out.println("发送通知给用户 " + userId + ": " + message);
          }
      }
      
  • 原理
    • @Lazy 注解告诉 Spring 在第一次使用 orderService 时再创建 Bean,而不是在 UserServiceImpl 初始化时立即创建。
  • 优点
    • 代码改动小,适合临时解决。
  • 缺点
    • 只是治标不治本,循环依赖仍然存在,可能隐藏设计问题。

3.3 方案 3:提取公共服务

  • 目标
    • 将通知逻辑提取到一个独立的服务中,减少 Service 之间的直接依赖。
  • 方法
    • 创建 NotificationService,专门负责通知逻辑。
    • UserServiceImpl 依赖 NotificationService,而不是 OrderServiceImpl
  • 实现
    • 创建 NotificationService
      @Service
      public class NotificationService {
      
          @Autowired
          private OrderMapper orderMapper;
      
          public void sendOrderNotification(String userId, String userName) {
              OrderEntity latestOrder = orderMapper.findLatestOrderByUserId(userId);
              if (latestOrder != null) {
                  System.out.println("用户 " + userName + " 的最近订单: " + latestOrder.getId());
              }
          }
      }
      
    • 修改 UserServiceImpl
      @Service
      public class UserServiceImpl implements UserService {
      
          @Autowired
          private UserMapper userMapper;
      
          @Autowired
          private NotificationService notificationService;
      
          @Override
          public UserEntity findById(String userId) {
              UserEntity user = userMapper.selectById(userId);
              if (user != null) {
                  notificationService.sendOrderNotification(userId, user.getName());
              }
              return user;
          }
      
          public void sendNotification(String userId, String message) {
              System.out.println("发送通知给用户 " + userId + ": " + message);
          }
      }
      
  • 优点
    • 提高了代码的可维护性和复用性,通知逻辑可以被其他服务复用。
    • 彻底消除循环依赖。
  • 缺点
    • 需要新增一个服务类,增加了代码量。

3.4 方案 4:允许循环依赖(不推荐)

  • 目标
    • 临时解决循环依赖问题,允许 Spring 处理循环依赖。
  • 方法
    • application.propertiesapplication.yml 中设置:
      spring.main.allow-circular-references=true
      
  • 原理
    • Spring 通过代理模式解决循环依赖。
  • 优点
    • 改动最小,适合快速验证。
  • 缺点
    • 掩盖了设计问题,可能导致后续维护困难。
    • 不符合 Spring 的最佳实践。

4. 推荐方案

  • 优先推荐方案 1(重构依赖关系)
    • 直接通过 OrderMapper 查询订单信息,移除 UserServiceImplOrderServiceImpl 的依赖。
    • 简单直接,符合依赖倒挂原则。
  • 次优选择方案 3(提取公共服务)
    • 如果通知逻辑在多个地方使用,提取 NotificationService 是一个更好的设计。
    • 提高了代码的可维护性和复用性。
  • 不推荐方案 4(允许循环依赖)
    • 只是临时解决方案,可能隐藏设计问题。

5. 学习总结

  • 循环依赖的成因
    • Service 之间双向依赖(例如 OrderServiceImplUserServiceImpl 互相依赖)。
    • Spring Boot 2.6 及之后默认禁止循环依赖。
  • 解决方法
    • 重构依赖关系:通过 Mapper 直接访问数据,移除 Service 之间的依赖。
    • 使用 @Lazy:延迟加载依赖,临时解决。
    • 提取公共服务:将共享逻辑提取到独立服务中。
    • 允许循环依赖:设置 spring.main.allow-circular-references=true(不推荐)。
  • 最佳实践
    • 避免 Service 之间直接依赖,优先通过 Mapper 访问数据。
    • 如果需要共享逻辑,提取公共服务或工具类。
    • 编写单元测试,确保重构后功能正常。

7. 补充

  • 代码设计
    • 遵循单一职责原则(SRP),避免 Service 之间耦合过高。
    • 使用依赖倒挂原则(DIP),Service 层依赖 Mapper 而不是其他 Service。
  • 测试
    • 编写单元测试和集成测试,确保重构后功能正常。
    • 模拟循环依赖场景,验证解决方案的有效性。
  • 工具
    • 使用 Spring 的依赖分析工具(例如 spring-boot-actuator)查看 Bean 依赖关系。
    • 在 IDE 中使用依赖图工具(如 IntelliJ IDEA 的 Dependency Matrix)分析循环依赖。

你可能感兴趣的:(SpringBoot,spring,boot,笔记,后端,java,ide,intellij-idea,spring)