Spring中循环依赖问题的解决机制总结

一、解决机制

1. 什么是循环依赖

循环依赖是指两个或多个Bean之间相互依赖对方,形成一个闭环的依赖关系。最常见的情况是当Bean A依赖Bean B,而Bean B又依赖Bean A时,就形成了循环依赖。在Spring容器初始化过程中,如果不加以特殊处理,这种循环依赖会导致Bean的创建过程陷入死循环,最终导致应用启动失败。

例如以下代码展示了一个典型的循环依赖场景:

@Service
public class A {@Autowiredprivate B b;
}@Service
public class B {@Autowiredprivate A a;
}

2. Spring三级缓存机制概述

Spring框架通过巧妙的"三级缓存"机制解决了循环依赖问题。这三级缓存在Spring源码中是通过三个Map集合实现的:

// 一级缓存:存放完全初始化好的Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);// 二级缓存:存放原始的Bean对象(尚未填充属性),用于解决循环依赖
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);// 三级缓存:存放Bean工厂对象,用于解决循环依赖
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

这三级缓存的作用分别是:

  • 一级缓存(singletonObjects):用于存储完全初始化好的Bean,即已经完成实例化、属性填充和初始化的Bean,可以直接被其他对象使用。

  • 二级缓存(earlySingletonObjects):用于存储早期曝光的Bean对象,这些Bean已经完成实例化但还未完成属性填充和初始化。当出现循环依赖时,其他Bean可以引用这个未完全初始化的Bean。

  • 三级缓存(singletonFactories):用于存储Bean的工厂对象,主要用于处理需要AOP代理的Bean的循环依赖问题。它存储的是一个Lambda表达式,这个表达式会返回Bean的早期引用,并在需要时将其放入二级缓存。

3. 三级缓存的查找顺序

当Spring需要获取一个单例Bean时,会按照以下顺序查找:

  1. 首先从一级缓存(singletonObjects)中查找,如果找到直接返回
  2. 如果一级缓存没有,再从二级缓存(earlySingletonObjects)中查找
  3. 如果二级缓存也没有,则从三级缓存(singletonFactories)中查找对应的工厂,如果找到则通过工厂获取对象,并将其放入二级缓存,同时从三级缓存中移除

这种层级查找的机制确保了在循环依赖的情况下,Bean能够被正确地创建和注入。

4. Spring解决循环依赖的详细流程

以A、B两个类相互依赖为例,Spring解决循环依赖的流程如下:

  1. 创建Bean A

    • Spring首先创建Bean A的实例(仅完成实例化,未进行属性填充)
    • 将A的创建工厂放入三级缓存singletonFactories中
    • 开始给A填充属性,发现依赖了B
  2. 创建Bean B

    • Spring开始创建B(因为A依赖B)
    • 实例化B,并将B的创建工厂放入三级缓存
    • 开始给B填充属性,发现依赖了A
  3. 处理循环依赖

    • 此时需要注入A,但A正在创建中
    • Spring尝试从一级缓存查找A,未找到
    • 继续从二级缓存查找A,未找到
    • 最后从三级缓存中找到A的工厂对象
    • 通过工厂获取A的早期引用(可能是原始对象,也可能是代理对象)
    • 将A的早期引用放入二级缓存,并从三级缓存中移除A的工厂
    • 将A的早期引用注入到B中
  4. 完成Bean创建

    • B完成属性填充和初始化,放入一级缓存
    • 返回到A的属性填充流程,将B注入到A中
    • A完成属性填充和初始化,放入一级缓存

通过这个流程,Spring成功解决了循环依赖问题,关键在于提前暴露了Bean的早期引用。

5. 为什么需要三级缓存

很多人会疑惑,为什么需要三级缓存?二级缓存不能解决循环依赖问题吗?

实际上,在不考虑AOP的情况下,二级缓存确实可以解决循环依赖问题。但Spring设计三级缓存的主要目的是为了处理AOP代理的情况。

在Spring中,AOP代理是在Bean生命周期的最后阶段(初始化后)创建的。但如果出现循环依赖,就需要提前创建代理对象。三级缓存中的工厂对象可以在需要时(即真正出现循环依赖时)才创建代理对象,而不是对所有Bean都提前创建代理。

这种设计有以下优势:

  1. 延迟代理对象的创建:只有在真正需要时才创建代理对象,避免了不必要的性能开销
  2. 保持Bean生命周期的一致性:尽可能地保持Bean的标准生命周期流程
  3. 灵活处理各种代理场景:适应不同的AOP实现和代理方式

6. 三级缓存的局限性

虽然三级缓存机制能够解决大多数循环依赖问题,但它仍有一些局限性:

  1. 不能解决构造器注入的循环依赖:因为构造器注入发生在实例化阶段,此时Bean还未被放入三级缓存,所以无法解决

  2. 不能解决prototype作用域的循环依赖:三级缓存机制只对单例Bean有效,对于prototype作用域的Bean,Spring不会缓存其实例,因此无法解决其循环依赖

  3. 不能解决多例Bean之间的循环依赖:原因同上,Spring不会缓存非单例Bean

7. 源码分析

Spring解决循环依赖的核心源码主要在DefaultSingletonBeanRegistry类中,关键方法包括:

  1. getSingleton(String beanName):按照一级、二级、三级缓存的顺序查找Bean
  2. addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory):将Bean的工厂对象放入三级缓存
  3. doGetBean(String name, Class<T> requiredType, Object[] args, boolean typeCheckOnly):获取Bean的主要逻辑

AbstractAutowireCapableBeanFactorydoCreateBean方法中,会在Bean实例化后立即将其工厂对象放入三级缓存,为解决循环依赖做准备。

8. 总结

Spring通过三级缓存机制巧妙地解决了循环依赖问题,其核心思想是将Bean的实例化和初始化分离,提前暴露实例化但未完全初始化的对象。三级缓存的设计不仅解决了基本的循环依赖问题,还优雅地处理了AOP代理场景下的循环依赖。

这种机制体现了Spring框架设计的精妙之处,通过缓存分层和提前暴露对象的方式,在不影响Bean正常生命周期的前提下解决了看似棘手的循环依赖问题。

二、实际示例与解析

为了更直观地理解Spring如何通过三级缓存解决循环依赖问题,我们来看一个具体的示例,并详细分析整个过程。

1. 示例代码

首先,我们创建两个相互依赖的Service类:

@Service
public class UserService {@Autowiredprivate OrderService orderService;public void findUserOrders(Long userId) {System.out.println("查询用户订单");orderService.getOrdersByUserId(userId);}
}@Service
public class OrderService {@Autowiredprivate UserService userService;public List<Order> getOrdersByUserId(Long userId) {System.out.println("获取用户订单");return new ArrayList<>();}public void notifyUser(Long orderId) {System.out.println("通知用户订单状态");userService.findUserOrders(1L); // 调用UserService的方法}
}

在这个示例中,UserService依赖OrderService,而OrderService又依赖UserService,形成了典型的循环依赖。

2. 详细解析Spring处理流程

让我们详细分析Spring如何处理这个循环依赖:

2.1 创建UserService

当Spring容器启动时,会按照Bean定义顺序开始创建Bean。假设先创建UserService

1. 实例化UserService对象(仅调用构造函数,此时内部属性尚未赋值)
2. 将UserService实例的创建工厂添加到三级缓存(singletonFactories)中singletonFactories.put("userService", () -> getEarlyBeanReference(beanName, mbd, userService实例))
3. 开始填充UserService的属性,发现需要注入OrderService

2.2 创建OrderService

由于UserService依赖OrderService,Spring开始创建OrderService:

1. 实例化OrderService对象(仅调用构造函数)
2. 将OrderService实例的创建工厂添加到三级缓存中singletonFactories.put("orderService", () -> getEarlyBeanReference(beanName, mbd, orderService实例))
3. 开始填充OrderService的属性,发现需要注入UserService

2.3 处理循环依赖

此时出现了关键的循环依赖处理步骤:

1. OrderService需要注入UserService,但UserService正在创建中
2. Spring尝试从一级缓存(singletonObjects)查找UserService,未找到
3. 继续从二级缓存(earlySingletonObjects)查找UserService,未找到
4. 最后从三级缓存(singletonFactories)中找到UserService的工厂对象
5. 调用工厂对象的getObject()方法获取UserService的早期引用- 如果UserService需要被代理(如有@Transactional注解),此时会创建代理对象- 如果不需要代理,则返回原始对象
6. 将获取到的UserService早期引用放入二级缓存,同时从三级缓存中移除earlySingletonObjects.put("userService", userService早期引用)singletonFactories.remove("userService")
7. 将UserService的早期引用注入到OrderService中

2.4 完成Bean创建

接下来完成两个Bean的创建过程:

1. OrderService完成属性填充(已注入UserService的早期引用)
2. OrderService完成初始化(调用各种初始化方法)
3. 如果OrderService需要被代理,创建代理对象(AOP)
4. 将完全初始化好的OrderService放入一级缓存singletonObjects.put("orderService", 完全初始化的orderService)
5. 返回到UserService的属性填充流程,将完全初始化好的OrderService注入到UserService中
6. UserService完成初始化
7. 如果UserService需要被代理且之前没有提前创建代理,创建代理对象
8. 将完全初始化好的UserService放入一级缓存singletonObjects.put("userService", 完全初始化的userService)

3. 关键源码执行分析

让我们看一下在这个过程中涉及的关键源码执行流程:

3.1 从getSingleton方法开始

当Spring尝试获取一个Bean时,首先会调用getSingleton方法:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {// 首先从一级缓存查找Object singletonObject = this.singletonObjects.get(beanName);// 如果一级缓存没有,且该Bean正在创建中if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {// 从二级缓存查找singletonObject = this.earlySingletonObjects.get(beanName);// 如果二级缓存也没有,且允许早期引用if (singletonObject == null && allowEarlyReference) {// 从三级缓存获取工厂ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {// 通过工厂获取早期引用singletonObject = singletonFactory.getObject();// 放入二级缓存this.earlySingletonObjects.put(beanName, singletonObject);// 从三级缓存移除this.singletonFactories.remove(beanName);}}}return singletonObject;
}

3.2 添加Bean到三级缓存

在Bean实例化后,Spring会将其添加到三级缓存中:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {// 如果一级缓存中不存在if (!this.singletonObjects.containsKey(beanName)) {// 添加到三级缓存this.singletonFactories.put(beanName, singletonFactory);// 确保二级缓存中不存在this.earlySingletonObjects.remove(beanName);// 记录注册的单例this.registeredSingletons.add(beanName);}}
}

3.3 创建早期引用

当需要处理循环依赖时,Spring会通过getEarlyBeanReference方法获取早期引用:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (BeanPostProcessor bp : getBeanPostProcessors()) {if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;// 这里可能创建代理对象exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);}}}return exposedObject;
}

4. AOP场景下的循环依赖

特别值得注意的是,当涉及到AOP代理时,循环依赖的处理会更加复杂。例如,如果我们的示例中添加了事务注解:

@Service
@Transactional
public class UserService {// ...
}@Service
@Transactional
public class OrderService {// ...
}

在这种情况下:

  1. 当从三级缓存获取UserService的早期引用时,会通过AbstractAutoProxyCreator.getEarlyBeanReference方法创建代理对象
  2. 这个代理对象会被放入二级缓存,并最终注入到OrderService中
  3. 这就确保了OrderService依赖的是UserService的代理对象,而不是原始对象

这也解释了为什么需要三级缓存而不是二级缓存:三级缓存中存储的工厂可以在需要时(出现循环依赖时)才创建代理对象,而不是对所有Bean都提前创建代理。

5. 总结

通过这个实际示例,我们可以清晰地看到Spring如何通过三级缓存机制解决循环依赖问题:

  1. 实例化Bean后立即将其工厂对象放入三级缓存
  2. 当出现循环依赖时,通过三级缓存获取早期引用(可能是代理对象)
  3. 将早期引用放入二级缓存,并从三级缓存中移除
  4. 使用早期引用完成依赖注入
  5. 最终将完全初始化的Bean放入一级缓存

这种机制既解决了循环依赖问题,又保持了Spring Bean生命周期的完整性,同时还能灵活处理AOP代理场景,体现了Spring框架设计的精妙之处。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/908496.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

集运维_安装linux,麒麟等系统_步骤

u盘工具选择Ventoy,Rufus 在选择Ventoy和Rufus这两款U盘启动盘制作工具时,需根据具体需求权衡其优缺点: ‌核心差异‌ ‌多系统支持‌: ‌Ventoy‌:支持将多个ISO、WIM、IMG等类型的镜像文件直接复制到U盘,实现‌一盘多用‌(例如同时存放Windows、Linux等镜像),无需…

第4章:Cypher查询语言基础

Cypher是Neo4j的声明式图查询语言&#xff0c;专为处理图数据而设计。它允许用户以直观、高效的方式查询和修改图数据库中的数据。本章将介绍Cypher的基本概念和语法&#xff0c;帮助读者掌握使用Cypher进行基础图数据操作的能力。 4.1 Cypher语言概述 Cypher是Neo4j的主要查…

上位机知识篇---Flask框架实现Web服务

本文将简单介绍Web 服务与前端显示部分,它们基于Flask 框架和HTML/CSS/JavaScript实现,主要负责将实时视频流和检测结果通过网页展示,并提供交互式状态监控。以下是详细技术解析: 一、Flask Web 服务架构 1. 核心路由设计 @app.route(/) def index():"""…

Neovim - 打造一款属于自己的编辑器(一)

文章目录 前言&#xff08;劝退&#xff09;neovim 安装neovim 配置配置文件位置第一个 hello world 代码拆分 neovim 配置正式配置 neovim基础配置自定义键位Lazy 插件管理器配置tokyonight 插件配置BufferLine 插件配置自动补全括号 / 引号 插件配置 前言&#xff08;劝退&am…

按字典序排列最小的等效字符串

文章目录 题目思路解题过程Python代码C代码复杂度 题目 给出长度相同的两个字符串s1 和 s2 &#xff0c;还有一个字符串 baseStr 。 其中 s1[i] 和 s2[i] 是一组等价字符。 举个例子&#xff0c;如果 s1 “abc” 且 s2 “cde”&#xff0c;那么就有 ‘a’ ‘c’, ‘b’ ‘…

Ubuntu2404 下搭建 Zephyr 开发环境

1. 系统要求 操作系统&#xff1a;Ubuntu2404&#xff08;64位&#xff09;磁盘空间&#xff1a;至少 8GB 可用空间&#xff08;Zephyr 及其工具链较大&#xff09; 2. 安装必要工具 Tool Min. Version CMake 3.20.5 Python 3.10 Devicetree compiler 1.4.6 2.1 安装系…

2025年06月07日Github流行趋势

项目名称&#xff1a;netbird 项目地址url&#xff1a;https://github.com/netbirdio/netbird项目语言&#xff1a;Go历史star数&#xff1a;14824今日star数&#xff1a;320项目维护者&#xff1a;mlsmaycon, braginini, pascal-fischer, lixmal, pappz项目简介&#xff1a;使…

fast-reid部署

配置设置&#xff1a; 官方库链接&#xff1a; https://github.com/JDAI-CV/fast-reid# git clone https://github.com/JDAI-CV/fast-reid.git 安装依赖&#xff1a; pip install -r docs/requirements.txt 编译&#xff1a;切换到fastreid/evaluation/rank_cylib目录下&a…

clickhouse 和 influxdb 选型

以下是 ClickHouse、InfluxDB 和 HBase 在体系架构、存储引擎、数据类型、性能及场景的详细对比分析: 🏗️ ‌一、体系架构对比‌ ‌维度‌‌ClickHouse‌‌InfluxDB‌‌HBase‌‌设计目标‌大规模OLAP分析,高吞吐复杂查询 时序数据采集与监控,优化时间线管理高吞吐随机…

ros创建工作空间配置运行状态机

ROS 一、创建工作空间目录 /home/wict/workspace/hudahua/ros/catkin_ws #初始化工作空间&#xff08;仅需一次&#xff09; catkin_init_workspace二&#xff1a;回到根目录编译 #创建正确的工作空间结构&#xff08;如果尚未创建&#xff09; mkdir -p ~/workspace/hudahua…

【看到哪里写到哪里】C的“数组指针”

C里面&#xff0c;数组指针&#xff0c;不是基本类型。顾名思义&#xff0c;数组指针&#xff0c;是指针&#xff0c;是指向数组的指针&#xff1b; 1.它的基本定义样子是 type (*ptr)[size]; 这个指针&#xff0c;指向的数组的&#xff1b;这里要注意&#xff0c;要定义数组…

深度相机的日常学习

文章目录 一、深度相机的概念二、深度相机的工作原理三、深度相机的应用领域 一、深度相机的概念 深度相机&#xff08;Depth Camera&#xff09;是一种能够捕捉场景中物体距离信息的设备&#xff0c;与传统的 RGB 相机不同&#xff0c;深度相机不仅可以获取场景的二维图像信息…

elasticsearch基本操作笔记

1.通过kibana查看elasticsearch版本信息 a.左上角三道横->Management->Dev Tools b.GET / 执行 c.执行结果 { “name” : “xxxx”, “cluster_name” : “xxxxxxx”, “cluster_uuid” : “vl1UudAoQp-aHWAzyPoMyw”, “version” : { “number” : “7.15.1”, “build…

两种Https正向代理的实现原理

正向代理 HTTPS 主要有两种方案&#xff0c;分别是基于证书的解密与再加密方案和基于 HTTP CONNECT 隧道的方案&#xff0c;以下是这两种方案的具体信息&#xff1a; 一、基于证书的解密与再加密方案 原理 工作原理&#xff1a;代理服务器拥有自己的证书&#xff0c;客户端需…

服务器健康摩尔斯电码:深度解读S0-S5状态指示灯

当服务器机柜中闪烁起神秘的琥珀色灯光&#xff0c;运维人员的神经瞬间绷紧——这些看似简单的Sx指示灯&#xff0c;实则是服务器用硬件语言发出的求救信号。掌握这套"摩尔斯电码"&#xff0c;等于拥有了预判故障的透视眼。 一、状态指示灯&#xff1a;服务器的生命体…

Java高级 | 【实验七】Springboot 过滤器和拦截器

隶属文章&#xff1a;Java高级 | &#xff08;二十二&#xff09;Java常用类库-CSDN博客 系列文章&#xff1a;Java高级 | 【实验一】Springboot安装及测试 |最新-CSDN博客 Java高级 | 【实验二】Springboot 控制器类相关注解知识-CSDN博客 Java高级 | 【实验三】Springboot 静…

【图片识别改名】如何批量将图片按图片上文字重命名?自动批量识别图片文字并命名,基于图片文字内容改名,WPF和京东ocr识别的解决方案

应用场景 在日常工作和生活中&#xff0c;我们经常会遇到需要对大量图片进行重命名的情况。例如&#xff0c;设计师可能需要根据图片内容为设计素材命名&#xff0c;文档管理人员可能需要根据扫描文档中的文字对图片进行分类命名。传统的手动重命名方式效率低下且容易出错&…

防火墙iptables项目实战

目录 一、网络规划 三、环境准备与检测 1、firewall &#xff08;1&#xff09;配置防火墙各大网卡ip并禁用firewalld和selinux &#xff08;2&#xff09;打开firewall路由转发 2、PC1&#xff08;内网&#xff09; &#xff08;1&#xff09;配置ip并禁用firewalld和s…

阿里云域名怎么绑定

阿里云服务器绑定域名全攻略&#xff1a;一步步轻松实现网站“零”障碍上线&#xff01; 域名&#xff0c;您网站在云端的“身份证”&#xff01; 在数字化浪潮中&#xff0c;拥有一个属于自己的网站或应用&#xff0c;是个人展示、企业运营不可或缺的一环。而云服务器&#x…

从仿射矩阵得到旋转量平移量缩放量

仿射变换原理 仿射变换是一种线性变换,可以包括平移、旋转、缩放和剪切等操作。其一般公式可以表示为: $$\mathbf{x’} = A \mathbf{x} + \mathbf{b} ] 其中: (\mathbf{x}) 是输入向量,通常表示一个点在二维或三维空间中的坐标。(\mathbf{x’}) 是输出向量,表示经过仿射变…