SpringBoot中依赖循环问题的产生与解决方案

 更新时间:2025年12月01日 10:11:26   作者:保加利亚的风  
在使用SpringBoot开发项目时,我们经常会遇到一个令人头疼的问题——依赖循环,当两个或多个Bean相互依赖,形成闭环时,Spring容器就无法完成它们的初始化,本文将通过一个具体案例,深入剖析依赖循环是如何产生的,并提供多种实用的解决方案,需要的朋友可以参考下

引言

在使用 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 开发中的常见陷阱,但它也是一面镜子,反映出我们代码设计中的耦合问题。与其寻找“绕过”的技巧,不如借此机会优化架构,写出更清晰、更健壮的代码。

到此这篇关于SpringBoot中依赖循环问题的产生与解决方案的文章就介绍到这了,更多相关SpringBoot依赖循环问题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot中@Import注解的使用方式

    SpringBoot中@Import注解的使用方式

    这篇文章主要介绍了SpringBoot中@Import注解的使用方式,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-05-05
  • SpringBoot中Tomcat和SpringMVC整合源码分析

    SpringBoot中Tomcat和SpringMVC整合源码分析

    Tomcat和SpringMVC都是通过这样的方式进行集成的,SpringBoot出现之前SpringMVC项目是直接部署在Tomcat服务器中的,这篇文章主要介绍了SpringBoot中Tomcat和SpringMVC整合源码分析,需要的朋友可以参考下
    2022-07-07
  • SpringBoot+SpringSession+Redis实现session共享及唯一登录示例

    SpringBoot+SpringSession+Redis实现session共享及唯一登录示例

    这篇文章主要介绍了SpringBoot+SpringSession+Redis实现session共享及唯一登录示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • java Lombok之@Accessors用法及说明

    java Lombok之@Accessors用法及说明

    这篇文章主要介绍了java Lombok之@Accessors用法及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • MyBatis如何使用selectKey返回主键的值

    MyBatis如何使用selectKey返回主键的值

    这篇文章主要介绍了MyBatis如何使用selectKey返回主键的值,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • sky-take-out项目中Redis的使用示例详解

    sky-take-out项目中Redis的使用示例详解

    SpringCache是Spring的缓存抽象层,通过注解简化缓存管理,支持Redis等提供者,适用于方法结果缓存、更新和删除操作,但无法实现Redis的高级功能(如数据结构、事务、分布式锁),本文给大家介绍sky-take-out项目中Redis的使用,感兴趣的朋友一起看看吧
    2025-07-07
  • 关于ArrayList初始化容量的问题

    关于ArrayList初始化容量的问题

    这篇文章主要介绍了关于ArrayList初始化容量的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • java截取字符串中的指定字符的两种方法(以base64图片为例)

    java截取字符串中的指定字符的两种方法(以base64图片为例)

    本文介绍了使用Java截取字符串中指定字符的方法,通过substring索引和正则实现,文章详细介绍了实现步骤和示例代码,对于想要了解如何使用Java截取字符串指定字符的读者具有一定的参考价值
    2023-08-08
  • 详谈Java几种线程池类型介绍及使用方法

    详谈Java几种线程池类型介绍及使用方法

    下面小编就为大家带来一篇详谈Java几种线程池类型介绍及使用方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-03-03
  • Java实现对华北、华南、华东和华中四个区域的划分

    Java实现对华北、华南、华东和华中四个区域的划分

    在Java中,通过定义枚举类、编写主程序和进行测试,本文详细介绍了如何划分华北、华南、华东和华中四个区域,首先定义枚举类标识区域,然后通过主程序接收用户输入并返回相应区域,最后通过测试用例确保正确性,文章还介绍了甘特图和饼状图的使用
    2024-09-09

最新评论