SpringBoot中循环依赖的常见陷阱与解决方案

 更新时间:2025年05月21日 10:42:10   作者:悟能不能悟  
循环依赖指两个或多个Bean相互依赖对方,形成一个闭环,本文将深入探讨循环依赖的根源,分析Spring的解决策略,并提供多种实战解决方案,希望对大家有所帮助

引言

在Spring Boot开发中,你是否遇到过这样的错误信息?

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

这表示你的应用出现了循环依赖。尽管Spring框架通过巧妙的机制解决了部分循环依赖问题,但在实际开发中(尤其是使用构造器注入时),开发者仍需警惕此类问题。本文将深入探讨循环依赖的根源,分析Spring的解决策略,并提供多种实战解决方案。

一、什么是循环依赖

循环依赖指两个或多个Bean相互依赖对方,形成一个闭环。例如:

  • ​Bean A​ 的创建需要注入 ​Bean B​
  • ​Bean B​ 的创建又需要注入 ​Bean A​

此时,Spring容器在初始化Bean时会陷入“死循环”。以下是一个典型示例:

@Service
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB) { // 构造器注入ServiceB
        this.serviceB = serviceB;
    }
}
 
@Service
public class ServiceB {
    private final ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA) { // 构造器注入ServiceA
        this.serviceA = serviceA;
    }
}

启动应用时,Spring会抛出异常:

BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Requested bean is currently in creation

二、Spring如何解决循环依赖

Spring通过三级缓存机制解决单例Bean的循环依赖问题:

  • ​一级缓存​(singletonObjects):存放完全初始化好的Bean。
  • ​二级缓存​(earlySingletonObjects):存放提前曝光的半成品Bean(仅实例化,未填充属性)。
  • ​三级缓存​(singletonFactories):存放Bean的工厂对象,用于生成半成品Bean。

​解决流程​(以A和B相互依赖为例):

  • 创建A时,先实例化A(未填充属性),并将A的工厂放入三级缓存。
  • 填充A的属性时发现需要B,开始创建B。
  • 创建B时,实例化B后,发现需要A,此时从三级缓存中通过工厂获取A的半成品对象。
  • B完成初始化,放入一级缓存。
  • A继续填充B的实例,完成初始化,放入一级缓存。

​关键限制​:该机制仅支持单例Bean且通过属性注入的场景。​构造器注入会直接失败!

三、为何构造器注入会导致循环依赖失败

构造器注入要求Bean在实例化阶段立即获得依赖对象,而三级缓存机制需要在属性注入阶段解决依赖。因此,当两个Bean都使用构造器注入时,Spring无法提前曝光半成品Bean,导致循环依赖无法解决。

四、解决方案:打破循环依赖的四种方法

1. ​改用Setter/Field注入(谨慎使用)​​

将构造器注入改为Setter或字段注入,允许Spring延迟注入依赖:

@Service
public class ServiceA {
    private ServiceB serviceB;
    
    @Autowired // Setter注入
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

​优点​:快速解决问题。

​缺点​:破坏了不可变性(字段非final),且可能掩盖设计问题。

2. ​使用@Lazy延迟加载​

在依赖对象上添加@Lazy,告知Spring延迟初始化Bean:

@Service
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB; // 实际注入的是代理对象
    }
}

​原理​:Spring生成代理对象,只有在首次调用时才会真正初始化目标Bean。

​适用场景​:解决构造函数注入的循环依赖。

3. ​重新设计代码结构​

通过分层或提取公共逻辑,消除循环依赖:

​方案一​:引入中间层(如ServiceC),将A和B的共同依赖转移到C。

​方案二​:使用事件驱动(ApplicationEvent),解耦直接依赖。

// 事件驱动示例
@Service
public class ServiceA {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
 
    public void doSomething() {
        eventPublisher.publishEvent(new EventA());
    }
}
 
@Service
public class ServiceB {
    @EventListener
    public void handleEventA(EventA event) {
        // 处理事件
    }
}

4. ​使用ObjectProvider(推荐)​​

在构造器中注入ObjectProvider,按需获取依赖:

@Service
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ObjectProvider<ServiceB> serviceBProvider) {
        this.serviceB = serviceBProvider.getIfUnique();
    }
}

​优点​:保持构造器注入的不可变性,显式控制依赖获取时机。

​注意​:需确保依赖Bean存在且唯一。

五、最佳实践与预防措施

1.​优先使用构造器注入​:保持Bean的不可变性和明确依赖,但需警惕循环依赖。

2.​定期检测循环依赖​:

使用IDE插件(如IntelliJ的Circular Dependencies分析)。

通过Maven/Gradle插件(如spring-boot-dependencies-analysis)。

3.​代码分层规范​:

严格遵循分层架构(Controller → Service → Repository)。

避免同一层内的Bean相互依赖。

4.​单元测试验证​:编写集成测试,验证Bean的初始化过程。

@SpringBootTest
public class CircularDependencyTest {
    @Autowired
    private ApplicationContext context;
 
    @Test
    void contextLoads() {
        // 若启动无异常,则通过测试
        assertNotNull(context.getBean(ServiceA.class));
    }
}

六、总结

循环依赖是Spring开发中的常见陷阱,其本质是代码设计问题。尽管Spring提供了部分解决方案,但重构代码消除循环依赖才是根本之道。通过合理使用注入方式、代码分层和工具检测,开发者可以有效避免此类问题,构建高可维护性的应用。

​记住​:

  • 慎用@Lazy和Setter注入,它们可能掩盖设计缺陷。
  • 构造器注入 + 合理分层 = 更健壮的系统!

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

相关文章

  • 基于springboot+vue实现垃圾分类管理系统

    基于springboot+vue实现垃圾分类管理系统

    这篇文章主要为大家详细介绍了基于springboot+vue实现垃圾分类管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-07-07
  • win10 java(jdk安装)环境变量配置和相关问题

    win10 java(jdk安装)环境变量配置和相关问题

    这篇文章主要介绍了win10java(jdk安装)环境变量配置和相关问题解决,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-12-12
  • SpringBatch跳过异常和限制方式

    SpringBatch跳过异常和限制方式

    这篇文章主要介绍了SpringBatch跳过异常和限制方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • SpringBoot基于Redis实现短信登录的操作

    SpringBoot基于Redis实现短信登录的操作

    验证码登录是非常常见的一种登录方式,能够简化用户登录的过程,本文主要介绍了SpringBoot基于Redis实现短信登录的操作,具有一定的参考价值,感兴趣的可以了解一下
    2023-12-12
  • Java中&和&&的区别简单介绍

    Java中&和&&的区别简单介绍

    这篇文章主要介绍了Java中&和&&的区别,&&逻辑与||逻辑或  它们都是逻辑运算符,& 按位与|按位或它们都是位运算符,更多详细内容请需要的小伙伴了解下面文章内容
    2022-01-01
  • Java中不用第三个变量来互换两个变量的值

    Java中不用第三个变量来互换两个变量的值

    在程序运行期间,随时可能产生一些临时数据,应用程序会将这些数据保存在一些内存单元中,每个内存单元都用一个标识符来标识。这些内存单元被称为变量,定义的标识符就是变量名,内存单元中存储的数据就是变量的值
    2021-10-10
  • Spring Boot 如何正确读取配置文件属性

    Spring Boot 如何正确读取配置文件属性

    这篇文章主要介绍了Spring Boot 如何正确读取配置文件属性,项目中经常会经常读取配置文件中的属性的值,Spring Boot提供了很多注解读取配置文件属性,那么如何正确使用呢,下文一起来参考下面文章内容吧
    2022-04-04
  • Java连接Redis全过程讲解

    Java连接Redis全过程讲解

    这篇文章主要介绍了Java连接Redis全过程讲解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • Spring Boot Admin 环境搭建与基本使用详解

    Spring Boot Admin 环境搭建与基本使用详解

    这篇文章主要介绍了Spring Boot Admin 环境搭建与基本使用,本文主要是对于Spring Boot Admin的基本认识和基本运用,通过本篇博客能够对Spring Boot Admin有一个宏观认知和能够快速上手,需要的朋友可以参考下
    2023-08-08
  • java线程并发blockingqueue类使用示例

    java线程并发blockingqueue类使用示例

    BlockingQueue是一种特殊的Queue,若BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态直到BlocingkQueue进了新货才会被唤醒,下面是用BlockingQueue来实现Producer和Consumer的例子
    2014-01-01

最新评论