深入剖析Spring如何解决循环依赖

 更新时间:2025年04月04日 08:15:59   作者:北辰alk  
循环依赖(Circular Dependency)是指两个或多个Bean相互依赖,形成一个闭环的情况,本文将和大家深入探讨一下Spring如何解决循环依赖,需要的可以参考下

一、什么是循环依赖

循环依赖(Circular Dependency)是指两个或多个Bean相互依赖,形成一个闭环的情况。例如:

@Service
public class AService {
    @Autowired
    private BService bService;
}

@Service
public class BService {
    @Autowired
    private AService aService;
}

上述代码中,AService依赖BService,而BService又依赖AService,形成了循环依赖。

二、Spring循环依赖的三种情况

构造器循环依赖:通过构造器注入形成的循环依赖,Spring无法解决,会直接抛出BeanCurrentlyInCreationException

Setter循环依赖(单例模式):通过setter方法注入形成的循环依赖,Spring可以解决
原型模式(prototype)循环依赖:scope为prototype的bean形成的循环依赖,Spring无法解决

三、Spring解决循环依赖的核心思想

Spring通过三级缓存机制来解决单例模式下的Setter循环依赖问题。核心思想是:

提前暴露未完全初始化的Bean实例:在Bean创建过程中,在完成实例化后、初始化前,将Bean的引用提前暴露

分级缓存:使用三级缓存存储不同状态的Bean,确保依赖注入时能获取到正确的引用

四、三级缓存详解

1. 三级缓存结构

/** 一级缓存:存放完全初始化好的Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 二级缓存:存放原始的Bean对象(尚未填充属性) */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

/** 三级缓存:存放Bean工厂对象,用于生成原始Bean对象 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

2. Bean创建与三级缓存交互流程

1.创建Bean实例:通过反射调用构造器创建Bean实例

2.放入三级缓存:将Bean包装成ObjectFactory并放入三级缓存singletonFactories

3.属性填充:填充Bean的属性,此时如果发现依赖其他Bean

  • 从一级缓存singletonObjects获取
  • 如果不存在,尝试从二级缓存earlySingletonObjects获取
  • 如果还不存在,从三级缓存singletonFactories获取ObjectFactory并生成Bean,然后放入二级缓存

4.初始化:执行初始化方法(@PostConstruct等)

5.放入一级缓存:将完全初始化好的Bean放入一级缓存,删除二、三级缓存中的记录

3. 循环依赖解决示例

以AService和BService的循环依赖为例:

1.开始创建AService

  • 实例化AService
  • 将AService的ObjectFactory放入三级缓存

2.填充AService属性时发现需要BService

  • 开始创建BService
  • 实例化BService
  • 将BService的ObjectFactory放入三级缓存

3.填充BService属性时发现需要AService

  • 从一级缓存获取AService → 不存在
  • 从二级缓存获取AService → 不存在
  • 从三级缓存获取AService的ObjectFactory → 存在,生成AService早期引用并放入二级缓存
  • 将AService早期引用注入BService

4.BService继续完成属性填充和初始化

5.BService创建完成,放入一级缓存

6.AService获得完全初始化的BService引用

7.AService继续完成属性填充和初始化

8.AService创建完成,放入一级缓存

五、源码分析

关键源码在DefaultSingletonBeanRegistry类中:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 先从一级缓存获取
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 2. 从二级缓存获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 3. 从三级缓存获取ObjectFactory
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    // 将三级缓存提升到二级缓存
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

六、为什么构造器循环依赖无法解决

时序问题:构造器注入发生在实例化阶段,此时Bean尚未创建完成,无法提前暴露引用

缓存机制不适用:三级缓存机制依赖于在实例化后、初始化前暴露引用,而构造器注入时连实例都还没完全创建

七、为什么原型模式的循环依赖无法解决

生命周期不同:原型模式每次获取都创建新实例,Spring不缓存原型Bean

无法提前暴露引用:没有缓存机制支持,无法在创建过程中共享未完成初始化的引用

八、Spring解决循环依赖的局限性

只能解决单例模式的Setter注入循环依赖

大量使用循环依赖会导致代码结构混乱,增加维护难度

某些AOP场景下可能出现问题(需要特殊处理)

九、最佳实践

避免循环依赖:通过设计模式(如中介者模式、观察者模式)重构代码

使用Setter注入替代构造器注入:如果必须使用循环依赖

使用@Lazy注解:延迟加载依赖的Bean

@Service
public class AService {
    @Autowired
    @Lazy
    private BService bService;
}

使用ApplicationContextAware接口:手动获取依赖Bean

十、总结

Spring通过三级缓存机制巧妙地解决了单例模式下Setter注入的循环依赖问题,但其本质上是一种妥协方案。良好的系统设计应当尽量避免循环依赖,保持清晰的依赖关系。理解Spring解决循环依赖的机制,有助于我们更好地使用Spring框架,并在遇到相关问题时能够快速定位和解决。

到此这篇关于深入剖析Spring如何解决循环依赖的文章就介绍到这了,更多相关Spring解决循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java Socket长连接实现教程及标准示例

    Java Socket长连接实现教程及标准示例

    Java Socket长连接提供了一种持久通信方式,适用于需要低延迟和高效率的网络应用,本文将详细介绍如何创建Java Socket长连接的客户端和服务端,包括它们的工作原理、实现细节、异常处理以及性能优化,感兴趣的朋友跟随小编一起看看吧
    2025-09-09
  • Java多线程面试题之交替输出问题的实现

    Java多线程面试题之交替输出问题的实现

    本文主要介绍了Java多线程面试题之交替输出问题的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • 详解java==运算符和equals()方法的区别

    详解java==运算符和equals()方法的区别

    这篇文章主要介绍了java==运算符和equals()方法的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • Mac系统上安装 JDK 8 最稳最全教程(Homebrew 方式)

    Mac系统上安装 JDK 8 最稳最全教程(Homebrew 方式)

    Homebrew是一个开源的包管理器,专为macOS(尽管也可以在Linux上使用)设计,用来简化在Mac操作系统上安装软件的过程,这篇文章主要介绍了Mac系统上使用Homebrew方式安装JDK8最稳最全教程的相关资料,需要的朋友可以参考下
    2026-04-04
  • Java8新特性之lambda(动力节点Java学院整理)

    Java8新特性之lambda(动力节点Java学院整理)

    这篇文章主要介绍了Java8新特性之lambda(动力节点Java学院整理)表达式的相关知识,包括lambda语法方面的知识,非常不错,具有参考借鉴价值,需要的朋友参考下吧
    2017-06-06
  • Spring @RequestMapping 注解及使用技巧详解

    Spring @RequestMapping 注解及使用技巧详解

    @RequestMapping是Spring MVC 中定义请求映射规则的核心注解,用于将HTTP请求映射到Controller处理方法,下面给大家介绍Spring @RequestMapping 注解及使用技巧,感兴趣的朋友一起看看吧
    2025-06-06
  • 全方位解读JDK和JRE的区别及联系

    全方位解读JDK和JRE的区别及联系

    这篇文章主要介绍了JDK和JRE的区别及联系,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • Jedis操作Redis数据库的方法

    Jedis操作Redis数据库的方法

    这篇文章主要为大家详细介绍了Jedis操作Redis数据库的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-04-04
  • 详解SpringBoot初始教程之Tomcat、Https配置以及Jetty优化

    详解SpringBoot初始教程之Tomcat、Https配置以及Jetty优化

    本篇文章主要介绍了详解SpringBoot初始教程之Tomcat、Https配置以及Jetty优化,具有一定的参考价值,有兴趣的可以了解一下
    2017-09-09
  • springboot返回前端中文乱码的解决

    springboot返回前端中文乱码的解决

    这篇文章主要介绍了springboot返回前端中文乱码的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09

最新评论