Springboot怎么解决循环依赖(基于单例 Bean)

 更新时间:2025年05月27日 10:36:30   作者:YY-帆S  
循环依赖指两个或多个 Bean 之间相互依赖,导致在创建 Bean 的过程中出现“死循环”,增加了对象依赖的混乱性,依赖关系变得错综复杂,下面给大家介绍Springboot怎么解决循环依赖,感兴趣的朋友一起看看吧

一、什么是循环依赖?

循环依赖(Circular Dependency) 指两个或多个 Bean 之间相互依赖,导致在创建 Bean 的过程中出现“死循环”,增加了对象依赖的混乱性,依赖关系变得错综复杂。

常见三种类型的循环依赖:

类型举例Spring 是否能解决
构造器注入循环依赖A → B → A(构造方法注入)
Setter / 字段注入循环依赖A → B → A(@Autowired)
Prototype 范围循环依赖A(原型) → B(原型) → A

1. 构造器注入循环依赖(Spring ❌无法解决)

@Component
public class A {
    private final B b;
    @Autowired
    public A(B b) {
        this.b = b;
    }
}
@Component
public class B {
    private final A a;
    @Autowired
    public B(A a) {
        this.a = a;
    }
}

❌ 结果:

报错:BeanCurrentlyInCreationException: Requested bean is currently in creation: Is there an unresolvable circular reference?

原因:Spring 必须先构造 A 才能注入 B,但 B 的构造又依赖 A,导致死循环,无法通过三级缓存提前暴露 Bean

2. 字段(或 setter)注入循环依赖(Spring ✅能自动解决)

@Component
public class A {
    @Autowired
    private B b;
}
@Component
public class B {
    @Autowired
    private A a;
}

✅ 结果:

Spring 能自动解决,应用成功启动。

  • 原因:Spring 会先构造出一个“空的 A 实例”,将其工厂加入三级缓存,B 注入 A 时就能拿到早期引用,从而打破循环。
  • 前提:spring.main.allow-circular-references: true,必须开启情况下才能自动解决。然Spring 6.0 起(包括 Spring Boot 3.x)默认为false

3. 原型作用域的循环依赖(Spring ❌无法解决)

@Component
@Scope("prototype")
public class A {
    @Autowired
    private B b;
}
@Component
@Scope("prototype")
public class B {
    @Autowired
    private A a;
}

❌ 结果:

报错:BeanCurrentlyInCreationException(创建过程中找不到可注入的 Bean)

原因:Spring 不缓存 prototype Bean 的创建过程,无法通过三级缓存解决依赖链,原型 Bean 不参与依赖管理

二、Spring 如何解决循环依赖(基于单例 Bean)

Spring 采用一种经典的 三级缓存机制(3-level cache) 来解决循环依赖。这个机制存在于DefaultSingletonBeanRegistry中。

🚨 前提:仅对 @Scope("singleton") 且使用字段或 setter 注入有效!

1. Bean 创建流程概览(以 A → B → A 为例)

✅ Step-by-step:

创建 A 实例(构造函数执行);

A被标记为“正在创建”,并将一个工厂(ObjectFactory)放入三级缓存

A 依赖 B → Spring 创建 B;

B 构造完成,发现依赖 A → 尝试获取 A;

Spring 发现 A 正在创建 → 从三级缓存拿到 ObjectFactory 生成早期 A 对象 → 放入二级缓存

B 成功注入 A,初始化完成;

回到 A,完成初始化。

整个过程中 Spring 使用缓存提前暴露未完成的 A 实例,从而打破了循环。

2. 三级缓存详解

缓存层级名称描述作用
一级缓存singletonObjects完全初始化完成的 Bean最终返回 Bean 实例
二级缓存earlySingletonObjects早期曝光的 Bean 实例用于依赖注入
三级缓存singletonFactories创建早期 Bean 的工厂延迟暴露 Bean 引用,支持代理等

Spring 将 Bean 提前曝光的流程:

singletonFactories -> earlySingletonObjects -> singletonObjects

3. 核心方法说明(来自源码)

在 Spring 源码中,关键方法如下:

// DefaultSingletonBeanRegistry.java
// 一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
// 二级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>();
// 三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();
  • 在创建 Bean 前:Spring 把一个生成 Bean 的工厂方法放入三级缓存。
  • 在注入依赖时:发现依赖的是一个“正在创建”的 Bean,就会去三级缓存中拿工厂生产早期对象。
  • 最后再完成依赖注入,放入一级缓存,清除早期引用。

三、业务开发者解决循环依赖的方法

1、使用@Lazy懒加载依赖

使用`@Lazy`注解延迟注入依赖属性。

@Component
public class A {
    @Autowired
    @Lazy
    private B b;
}

2、将依赖的代码移入新类,打破依赖闭环。

A → MiddleService → B

3、在方法中动态调用spring容器的getBean方法获取依赖,达到延迟获取bean,避免类中直接注入循环依赖的bean

使用 ObjectFactory 或 ApplicationContext.getBean() 延迟获取 Bean

@Component
@Scope("prototype")
public class A {
    @Autowired
    private ObjectFactory<B> bFactory;
    public void use() {
        B b = bFactory.getObject(); // 延迟获取
    }
}

4、改为 setter 或字段注入(避免构造器注入)

构造器注入是“强依赖”,无法提前暴露:

@Component
public class A {
    private B b;
    @Autowired
    public void setB(B b) {
        this.b = b;
    }
}

5、使用@PostConstruct 或 工厂方法延迟注入

将依赖注入放到初始化之后:

@Component
public class A {
    private final B b;
    public A(B b) {
        this.b = b;
    }
    @PostConstruct
    public void init() {
        // 在这里安全使用 b
    }
}

6、开启Spring Boot循环依赖(不推荐,除非必要)。

spring:
    main:
        allow-circular-references: true

到此这篇关于Springboot怎么解决循环依赖(基于单例 Bean)的文章就介绍到这了,更多相关Springboot循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java利用移位运算将int型分解成四个byte型的方法

    Java利用移位运算将int型分解成四个byte型的方法

    今天小编就为大家分享一篇关于Java利用移位运算将int型分解成四个byte型的方法,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-12-12
  • Java中ArrayList类详细介绍

    Java中ArrayList类详细介绍

    这篇文章主要介绍了Java中ArrayList类详细介绍的相关资料,需要的朋友可以参考下
    2017-04-04
  • java如何获取指定文件夹下的所有文件名

    java如何获取指定文件夹下的所有文件名

    这篇文章主要介绍了java如何获取指定文件夹下的所有文件名问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Java图形化界面编程介绍

    Java图形化界面编程介绍

    这篇文章主要介绍了Java图形化界面编程,形化界面编程可以直接的看到每一步操作带来的效果,相对于传统编程盯着黑框框学起来是非常非常有意思的,想了解更多的小伙伴请参考下面文章的详细内容
    2022-01-01
  • Java注解Annotaton详解

    Java注解Annotaton详解

    Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制,文中给大家介绍了三种基本的Annotaton,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2022-05-05
  • SpringBoot+slf4j线程池全链路调用日志跟踪问题及解决思路(二)

    SpringBoot+slf4j线程池全链路调用日志跟踪问题及解决思路(二)

    本文主要给大家介绍如何实现子线程中的traceId日志跟踪,本文通过封装Callable为例给大家介绍的非常详细,需要的朋友一起看看吧
    2021-05-05
  • Java字符判断的小例子

    Java字符判断的小例子

    从键盘上输入一个字符串,遍历该字符串中的每个字符,若该字符为小写字母,则输出“此字符是小写字母”;若为大写字母,则输出“此字符为大写字母”;否则输出“此字符不是字母”
    2013-09-09
  • 解决springboot启动报错bean找不到的问题

    解决springboot启动报错bean找不到的问题

    这篇文章主要介绍了解决springboot启动报错bean找不到原因,本文给大家分享完美解决方案,通过图文相结合给大家介绍的非常详细,需要的朋友可以参考下
    2023-03-03
  • Java 判断字符串a和b是否互为旋转词

    Java 判断字符串a和b是否互为旋转词

    本篇文章主要介绍了判断字符串a和b是否互为旋转词的相关知识,具有很好的参考价值。下面跟着小编一起来看下吧
    2017-05-05
  • 深入Java7的一些新特性以及对脚本语言支持API的介绍

    深入Java7的一些新特性以及对脚本语言支持API的介绍

    本篇文章是对Java7的一些新特性以及对脚本语言支持API的概述,需要的朋友参考下
    2013-05-05

最新评论