Spring源码分析の循环依赖
文章目录
- 前言
- 一、循环依赖问题
- 二、循环依赖的解决
- 三、整体流程分析
前言
常见的可能存在循环依赖的情况如下:
- 两个bean中互相持有对方作为自己的属性。

类似于:

- 两个bean中互相持有对方作为自己的属性,且在构造时就需要传入:

类似于:

- 在某个bean中注入自身:

其中第二种构造方法的循环依赖一般情况下是无解的,除非加上@Lazy注解。本篇重点分析第一种循环依赖Spring是如何解决的。
一、循环依赖问题
Spring在创建一个bean时,简单来说会经过实例化bean,属性注入,初始化的操作。当出现第一种循环依赖时,可能会经历以下的过程:
AService
- 去单例池中找有无AService实例,此时没有,执行
doCreateBean。 createBeanInstance创建出AService实例。- 执行AService实例的属性填充。
- AService的初始化、初始化前。
- AService的初始化后。
- 放入单例池。
其中在执行AService实例的属性填充这一步,会根据@AutoWired的注入点,去寻找BService实例:
- 去单例池中找有无BService实例,此时没有,执行
doCreateBean。 - createBeanInstance`创建出BService实例。
- 执行BService实例的属性填充。
- BService的初始化、初始化前。
- BService的初始化后。
- 放入单例池。
其中在执行BService实例的属性填充这一步,会根据@AutoWired的注入点,发现需要填充AService实例,这就出现了循环依赖的问题。
二、循环依赖的解决
那么Spring是如何解决循环依赖的?主要是通过三级缓存的机制去实现的:
singletonObjects :一级缓存 earlySingletonObjects 二级缓存 singletonFactories 三级缓存
可以看到,首先在doCreateBean的方法中,bean实例化后,属性填充之前,先有一段图上的逻辑:
- 判断当前的bean是否是单例的,以及是否支持循环依赖(默认是true),以及
singletonsCurrentlyInCreation集合中是否包含当前bean。 - 如果满足条件,就把当前的bean的名称,和一段lambda表达式,放入
singletonFactories集合中。
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {//单例池中没有该beanif (!this.singletonObjects.containsKey(beanName)) {//向单例工厂缓存当前bean名称以及对应的lambda表达式this.singletonFactories.put(beanName, singletonFactory);//从早期单例对象的缓存中去除当前的beanthis.earlySingletonObjects.remove(beanName);//向已注册的单例bean集合中添加当前bean名称this.registeredSingletons.add(beanName);}}}
这里的lambda表达式,主要是为了返回一个可以被外部提前访问的 bean 实例。注意:lambda表达式不是在此处执行,而是先放入了earlySingletonObjects集合中!
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {//如果 Spring AOP 代理在这里介入,那么 getEarlyBeanReference() 可能会返回一个动态代理对象,而不是原始 beanexposedObject = bp.getEarlyBeanReference(exposedObject, beanName);}}return exposedObject;}
返回可能经过 AOP 代理的 bean
判断是否需要AOP
并且在执行doGetBean时,会首先执行getSingleton方法:

在getSingleton方法中,运用了双检锁模式,避免在加锁后其它线程已经创建并缓存了该 bean。并且上面提到的lambda表达式,会在此处真正地去执行
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {// 尝试从单例池中获取当前 bean 的实例,避免加锁操作,提高性能Object singletonObject = this.singletonObjects.get(beanName);// 如果单例池(一级缓存)中没有找到,并且当前 bean 正在创建过程中(循环依赖的处理)if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {// 尝试从早期单例池(二级缓存)中获取当前 bean(即尚未完全初始化的对象)singletonObject = this.earlySingletonObjects.get(beanName);// 如果在早期单例池中也没有找到,并且允许获取早期引用(通常是为了解决循环依赖)if (singletonObject == null && allowEarlyReference) {// 加锁,避免并发创建同一个 bean 导致不一致的问题synchronized (this.singletonObjects) {// 再次检查,避免在加锁后其它线程已经创建并缓存了该 beansingletonObject = this.singletonObjects.get(beanName);// 如果单例池中依然没有找到该 beanif (singletonObject == null) {// 尝试从早期单例池中获取,如果仍然没有找到singletonObject = this.earlySingletonObjects.get(beanName);// 如果早期单例池中也没有找到,尝试从工厂中获取 bean(即懒加载)if (singletonObject == null) {// 获取 bean 的工厂方法(三级缓存)ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);// 如果工厂方法存在,说明该 bean 尚未初始化,且支持懒加载if (singletonFactory != null) {// 使用工厂创建 bean 并缓存到早期单例池中,防止循环依赖singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject); // 缓存早期对象this.singletonFactories.remove(beanName); // 移除工厂方法,因为 bean 已经创建}}}}}}// 返回获取到的 singletonObject,如果没有找到,则返回 nullreturn singletonObject;
}
这里的isSingletonCurrentlyInCreation方法,是判断当前的bean名称是否在singletonsCurrentlyInCreation集合中,那么bean是在什么时候存入该集合的呢?答案是在下方重载的getSingleton方法中:


无论if条件是否成立,都会把当前的bean名称放入singletonsCurrentlyInCreation集合中
而在执行完createBean(包括实例化,依赖注入,初始化)之后,会执行addSingleton方法:

会将当前bean从二级、三级缓存中移除,并放入单例池中,表示该bean已经完全创建完成。
protected void addSingleton(String beanName, Object singletonObject) {synchronized (this.singletonObjects) {//将当前初始化完成的bean存入单例池(一级缓存)this.singletonObjects.put(beanName, singletonObject);//从三级缓存中删除当前beanthis.singletonFactories.remove(beanName);//从二级缓存中删除当前beanthis.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}
}
三、整体流程分析
A和B的创建流程 蓝色代表A 绿色代表B
A尝试从单例池中获取
条件不满足,返回null
进入getSingleton

进入getSingleton,执行beforeSingletonCreation,将A放入singletonsCurrentlyInCreation中,代表A正在被创建。
进入doCreateBean的addSingletonFactory方法:
进入doCreateBean的addSingletonFactory方法,将A放入三级缓存中
执行A的属性填充(A有一个属性为B):
尝试从容器中获取B

这里的条件依旧不满足,返回null:
进入getSingleton,执行beforeSingletonCreation,同样将B放入singletonsCurrentlyInCreation中,代表B正在被创建。
进入doCreateBean的addSingletonFactory
同样将B放入三级缓存

此时的缓存情况:A和B只在二级缓存中

进行B的属性填充**(B中有一个A属性)**
再次尝试从容器中获取A

此时的条件满足:
从三级缓存中取出A的lambda表达式执行:

此时的A的B属性是没有值的
放入二级缓存,并从三级缓存中删除:

此时的缓存情况:

直接返回,不走createBean的逻辑了。

给B的A属性赋值,完成属性填充:
B继续执行初始化
执行B的getSingleton的addSingleton方法,将B放入单例池,并且清除二三级缓存:
回到A的属性填充,填充了B属性
填充完成后,A的B属性有值了,B的A属性也有值了,继续执行A的初始化,完成后将A放入单例池,并且清除二三级缓存:
此时的二三级缓存全部清空:

至此整个流程全部结束。
为什么要加入三级缓存?因为上面的过程只是普通情况,还需要考虑到AOP的情况。如果开启了AOP,那么会在初始化后,**基于切面生成一个代理对象。**而循环依赖的触发时机是在属性注入时,如果只使用普通的缓存,会导致解决循环依赖时注入的对象是普通对象,而最终的对象是代理对象,产生不一致的情况。
applyBeanPostProcessorsAfterInitialization是初始化后执行的方法,也是在AOP的场景下生成代理对象的方法:
如果开启了AOP,那么在循环中会执行AbstractAutoProxyCreator的postProcessAfterInitialization方法生成代理,在这一步中会判断当前Bean是否已经在解决了循环依赖的过程中进行了AOP,如果已经进行过了,就不会再次生成代理,保证代理对象的唯一性。

相关文章:
Spring源码分析の循环依赖
文章目录 前言一、循环依赖问题二、循环依赖的解决三、整体流程分析 前言 常见的可能存在循环依赖的情况如下: 两个bean中互相持有对方作为自己的属性。 类似于: 两个bean中互相持有对方作为自己的属性,且在构造时就需要传入:…...
检查SSH安全配置-关于“MaxStartups参数”
官方文档介绍 在《检查SSH安全配置-sshd服务端未认证连接最大并发量配置》中我们简略地阐述了“MaxStartups参数”在SSH安全配置中的意义。但是,并未对该参数做详细说明。 为啥没有详细说明呢?因为俺也没弄明白! 我们先看一下sshd_config的…...
某查”平台请求头反爬技术解析与应对
一、请求头反爬技术概述 请求头(HTTP Header)是 HTTP 协议中用于在客户端和服务器之间传递信息的一部分。它包含了请求的来源、用户代理、内容类型等关键信息。许多网站通过检查请求头中的特定字段来判断请求是否来自合法的浏览器,从而防止爬…...
MOE结构解读和deepseek的MoE结构
不管dense还是MoE(Mixture of Experts)都是基于transformer的。 下面回顾下解码器块的主要架构: 注意力机制-层归一化&残差连接-FFN前馈神经网络-层归一化&残差连接。 dense模型是沿用了这个一架构,将post-norm换为pre-no…...
LLM+多智能体协作:基于CrewAI与DeepSeek的邮件自动化实践
文章目录 引言理解 Flows(工作流)与 Crews(协作组)一、环境准备与工具安装1.1 Python环境搭建1.2 创建并激活虚拟环境1.3 安装核心依赖库(crewai、litellm) 二、本地DeepSeek R1大模型部署2.1 Ollama框架安…...
基于C++“简单且有效”的“数据库连接池”
前言 数据库连接池在开发中应该是很常用的一个组件,他可以很好的节省连接数据库的时间开销;本文基使用C实现了一个简单的数据库连接池,代码量只有400行只有,但是压力测试效果很好;欢迎收藏 关注,本人将会…...
为什么要将PDF转换为CSV?CSV是Excel吗?
在企业和数据管理的日常工作中,PDF文件和CSV文件承担着各自的任务。PDF通常用于传输和展示静态的文档,而CSV因其简洁、易操作的特性,广泛应用于数据存储和交换。如果需要从PDF中提取、分析或处理数据,转换为CSV格式可能是一个高效…...
Redis 集群的三种模式:一主一从、一主多从和多主多从
本文记述了博主在学习 Redis 在大型项目下的使用方式,包括如何设置Redis主从节点,应对突发状况如何处理。在了解了Redis的集群搭建和相关的主从复制以及哨兵模式的知识以后,进而想要了解 Redis 集群如何使用,如何正确使用…...
面试题——简述Vue 3的服务器端渲染(SSR)是如何工作的?
面试题——简述Vue3的服务器端渲染(SSR)是如何工作的? 服务器端渲染(SSR)已经成为了一个热门话题。Vue 3,作为一款流行的前端框架,也提供了强大的SSR支持。那么,Vue 3的SSR究竟是如…...
2.25DFS和BFS刷题
洛谷P1101单词方阵:用sta存字符串,for找到‘y的位置,然后dfs对字符串用for进行一个一个的判断,不符合就return,下面再用for进行book标记,能执行下面的for说明上面没有return,所以说明找到&#…...
C语言基本知识------指针(4)
1. 回调函数是什么? 回调函数就是⼀个通过函数指针调用的函数。 如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数 时,被调⽤的函数就是回调函数。 void qsort(void base,//指针…...
【OMCI实践】ONT上线过程的omci消息(六)
引言 在前四篇文章中,主要介绍了ONT上线过程的OMCI交互的第一、二、三个阶段omci消息,本篇介绍第四个阶段,OLT下发配置到ONT。前三个阶段,每个厂商OLT和ONT都遵循相同标准,OMCI的交换过程大同小异。但第四个阶段&…...
C语言(13)------------>do-while循环
1.do-while循环的语法 我们知道C语言有三大结构,顺序、选择、循环。我们可以使用while循环、for循环、do-while循环实现循环结构。之前的博客中提及到了前两者的技术实现。可以参考: C语言(11)------------->while循…...
腾讯SQL面试题解析:如何找出连续5天涨幅超过5%的股票
腾讯SQL面试题解析:如何找出连续5天涨幅超过5%的股票 作者:某七年数据开发工程师 | 2025年02月23日 关键词:SQL窗口函数、连续问题、股票分析、腾讯面试题 一、问题背景与难点拆解 在股票量化分析场景中,"连续N天满足条件"是高频面试题类型。本题要求在单表stoc…...
HybridCLR+Adressable+Springboot热更
本文章会手把手教大家如何搭建HybridCLRAdressableSpringboot热更。 创作不易,动动发财的小手点个赞。 安装华佗 首先我们按照官网的快速上手指南搭建一个简易的项目: 快速上手 | HybridCLR 注意在热更的代码里添加程序集。把用到的工具放到程序集里…...
电脑连接示波器显示波形
通过网线连接示波器和电脑,将示波器波形显示在电脑上直接复制图片至报告中,以下是配置步骤。 一、设备 网线,Tektronix示波器,电脑 二、使用步骤 1.用网线连接电脑和示波器 2.电脑关掉WiFi,查看IPv4网关地址…...
监听其他音频播放时暂停正在播放的音频
要实现当有其他音频播放时暂停当前音频,你可以使用全局事件总线或 Vuex 来管理音频播放状态。这里我将展示如何使用一个简单的事件总线来实现这个功能。 首先,你需要创建一个事件总线。你可以在项目的一个公共文件中创建它,例如 eventBus.js…...
小熊猫C++安装EasyX最新教程
1.下载EasyX 官网下载: EasyX 官网https://easyx.cn/ 2.将下载文件改格式解压 注意:下载文件为.exe格式,需将其格式改成.zip格式! 如何改格式? a.若文件名字未显示.exe (1).打开此电脑 (2).点击上端的查看 (…...
安装VM和Centos
安装VM 一、打开虚拟机 二、选择典型 三、选择光盘 四、指定虚拟机位置 五、设置磁盘大小并拆分为多个文件 六、完成 安装Centos 一、上述过程完成后我们直接打开虚拟机 二、语言选择中文 三、默认安装位置并点击完成 四、点击开始安装 五、点击设置密码 设置完密码后点击完成…...
git 命令 设置别名
在Git中,您可以通过以下命令查看所有的alias(别名): git config --get-regexp alias 这个命令会列出所有配置的alias,例如: alias.st.status alias.co.checkout alias.br.branch ... 如果您想查看某个特定a…...
uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖
在前面的练习中,每个页面需要使用ref,onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入,需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...
Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
Razor编程中@Html的方法使用大全
文章目录 1. 基础HTML辅助方法1.1 Html.ActionLink()1.2 Html.RouteLink()1.3 Html.Display() / Html.DisplayFor()1.4 Html.Editor() / Html.EditorFor()1.5 Html.Label() / Html.LabelFor()1.6 Html.TextBox() / Html.TextBoxFor() 2. 表单相关辅助方法2.1 Html.BeginForm() …...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...
go 里面的指针
指针 在 Go 中,指针(pointer)是一个变量的内存地址,就像 C 语言那样: a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10,通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...
十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建
【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...
Python竞赛环境搭建全攻略
Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型(算法、数据分析、机器学习等)不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...
