【Java】Spring Boot 中依赖循环问题的产生与解决方案详解

引言

在使用 Spring Boot 开发项目时,我们经常会遇到一个令人头疼的问题——依赖循环(Circular Dependency)。当两个或多个 Bean 相互依赖,形成闭环时,Spring 容器就无法完成它们的初始化,从而抛出 BeanCurrentlyInCreationException 异常。

本文将通过一个具体案例,深入剖析依赖循环是如何产生的,并提供多种实用的解决方案,帮助你彻底掌握这一常见问题的应对之道。


一、什么是依赖循环?

依赖循环是指两个或多个 Spring Bean 在构造函数或字段注入时互相依赖,形成一个闭环。例如:

  • A 依赖 B
  • B 依赖 A
    这种情况下,Spring 容器在创建 A 时需要先创建 B,而创建 B 又需要先创建 A,陷入死循环。

二、依赖循环的典型场景(案例)

场景描述

假设我们正在开发一个用户服务系统,有两个核心组件:

  • UserService:负责用户相关业务逻辑
  • OrderService:负责订单处理,其中需要验证用户信息
    两者相互调用对方的方法,导致循环依赖。

代码示例

@Service
public class UserService {

    private final OrderService orderService;

    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }

    public void handleUser() {
        System.out.println("Handling user...");
        orderService.checkOrders();
    }
}
@Service
public class OrderService {

    private final UserService userService;

    public OrderService(UserService userService) {
        this.userService = userService;
    }

    public void checkOrders() {
        System.out.println("Checking orders...");
        userService.handleUser(); // 实际中可能不是直接调用,但存在依赖关系
    }
}

启动报错

当你启动 Spring Boot 应用时,会看到类似如下错误:

Error creating bean with name 'userService': Requested bean is currently in creation: Is there an unresolvable circular reference?

这是因为 Spring 默认使用构造器注入(Constructor Injection),而构造器注入无法处理循环依赖(仅支持 setter 或 field 注入的三级缓存机制)。

三、为什么会出现依赖循环?

根本原因在于设计层面的耦合度过高。两个本应职责分离的服务,却互相持有对方的引用,违反了“单一职责原则”和“依赖倒置原则”。

虽然 Spring 框架在某些情况下(如 setter 注入)可以通过三级缓存机制解决循环依赖,但构造器注入 + 循环依赖 = 必然失败。

📌 注意:Spring 仅支持单例 Bean 的 setter/field 注入下的循环依赖,不支持原型(Prototype)Bean 或构造器注入的循环依赖。

四、解决方案详解

方案一:重构代码,消除循环依赖(推荐)

这是最根本、最优雅的解决方式。

思路:

  • 提取公共逻辑到第三个服务
  • 使用事件驱动(ApplicationEvent)
  • 通过接口解耦
示例:引入 UserValidator 服务
@Service
public class UserValidator {
    public boolean isValid(Long userId) {
        // 验证逻辑
        return true;
    }
}

@Service
public class UserService {
    private final UserValidator userValidator;

    public UserService(UserValidator userValidator) {
        this.userValidator = userValidator;
    }

    public void handleUser() {
        System.out.println("Handling user...");
    }
}

@Service
public class OrderService {
    private final UserValidator userValidator;

    public OrderService(UserValidator userValidator) {
        this.userValidator = userValidator;
    }

    public void checkOrders() {
        if (userValidator.isValid(1L)) {
            System.out.println("Orders are valid.");
        }
    }
}

优点:职责清晰,无循环,易于测试和维护。

方案二:使用 @Lazy 延迟加载(快速修复)

在其中一个注入点上添加 @Lazy 注解,让 Spring 在实际使用时才初始化该 Bean。

@Service
public class UserService {

    private final OrderService orderService;

    public UserService(@Lazy OrderService orderService) {
        this.orderService = orderService;
    }

    // ...
}

Spring 会在创建 UserService 时注入一个代理对象,真正调用方法时才初始化 OrderService

⚠️ 注意:这只是“绕过”问题,并未真正解决设计缺陷,仅适用于临时修复。

方案三:改用 Setter 或 Field 注入(不推荐)

将构造器注入改为 @Autowired 字段注入:

@Service
public class UserService {

    @Autowired
    private OrderService orderService;

    // ...
}

Spring 利用三级缓存(singletonObjects、earlySingletonObjects、singletonFactories)可以在这种情况下完成注入。

❌ 缺点:

  • 破坏了不可变性
  • 不利于单元测试
  • 违背 Spring 官方推荐的“优先使用构造器注入”原则

官方文档明确建议:尽可能使用构造器注入,因为它能保证依赖不为 null,且对象状态完整。

方案四:使用 ApplicationContext 手动获取 Bean(极端情况)

@Component
public class ServiceLocator implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        ServiceLocator.context = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return context.getBean(clazz);
    }
}

// 在 UserService 中
public void someMethod() {
    OrderService orderService = ServiceLocator.getBean(OrderService.class);
    orderService.doSomething();
}

⚠️ 此方法破坏了 IoC 原则,应尽量避免。

五、总结

方案是否推荐说明
重构代码(提取公共逻辑)✅ 强烈推荐从根源解决问题,提升架构质量
使用 @Lazy⚠️ 谨慎使用快速修复,但掩盖设计问题
改用字段注入❌ 不推荐违背最佳实践,降低代码质量
手动获取 Bean❌ 不推荐破坏依赖注入原则

最佳实践建议:

  • 优先使用构造器注入
  • 避免服务之间直接相互依赖
  • 通过领域事件、消息队列或中介服务解耦
  • 定期审查 Bean 依赖图(可使用 spring-boot-starter-actuator 的 /beans 端点)

六、结语
依赖循环是 Spring 开发中的常见陷阱,但它也是一面镜子,反映出我们代码设计中的耦合问题。与其寻找“绕过”的技巧,不如借此机会优化架构,写出更清晰、更健壮的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值