源码深度解析,Spring 如何解决循环依赖?
1. 基础知识1.1 什么是循环依赖 一个或多个对象之间存在直接或间接的依赖关系这种依赖关系构成一个环形调用有下面 3 种方式。我们看一个简单的 Demo对标“情况 2”。Service public class Louzai1 { Autowired private Louzai2 louzai2; public void test1() { } } Service public class Louzai2 { Autowired private Louzai1 louzai1; public void test2() { } }这是一个经典的循环依赖它能正常运行后面我们会通过源码的角度解读整体的执行流程。1.2 三级缓存解读源码流程之前spring 内部的三级缓存逻辑必须了解要不然后面看代码会蒙圈。第一级缓存singletonObjects用于保存实例化、注入、初始化完成的 bean 实例第二级缓存earlySingletonObjects用于保存实例化完成的 bean 实例第三级缓存singletonFactories用于保存 bean 创建工厂以便后面有机会创建代理对象。这是最核心我们直接上源码执行逻辑先从“第一级缓存”找对象有就返回没有就找“二级缓存”找“二级缓存”有就返回没有就找“三级缓存”找“三级缓存”找到了就获取对象放到“二级缓存”从“三级缓存”移除。1.3 原理执行流程我把“情况 2”执行的流程分解为下面 3 步是不是和“套娃”很像 整个执行逻辑如下在第一层中先去获取 A 的 Bean发现没有就准备去创建一个然后将 A 的代理工厂放入“三级缓存”这个 A 其实是一个半成品还没有对里面的属性进行注入但是 A 依赖 B 的创建就必须先去创建 B在第二层中准备创建 B发现 B 又依赖 A需要先去创建 A在第三层中去创建 A因为第一层已经创建了 A 的代理工厂直接从“三级缓存”中拿到 A 的代理工厂获取 A 的代理对象放入“二级缓存”并清除“三级缓存”回到第二层现在有了 A 的代理对象对 A 的依赖完美解决这里的 A 仍然是个半成品B 初始化成功回到第一层现在 B 初始化成功完成 A 对象的属性注入然后再填充 A 的其它属性以及 A 的其它步骤包括 AOP完成对 A 完整的初始化功能这里的 A 才是完整的 Bean。将 A 放入“一级缓存”。为什么要用 3 级缓存 我们先看源码执行流程后面我会给出答案。2. 源码解读注意Spring 的版本是5.2.15.RELEASE否则和我的代码不一样上面的知识网上其实都有下面才是我们的重头戏让你跟着楼仔走一遍代码流程。2.1 代码入口这里需要多跑几次把前面的 beanName 跳过去只看 louzai1。2.2 第一层进入 doGetBean()从 getSingleton() 没有找到对象进入创建 Bean 的逻辑。进入 doCreateBean() 后调用 addSingletonFactory()。往三级缓存 singletonFactories 塞入 louzai1 的工厂对象。进入到 populateBean()执行 postProcessProperties()这里是一个策略模式找到下图的策略对象。正式进入该策略对应的方法。下面都是为了获取 louzai1 的成员对象然后进行注入。进入 doResolveDependency()找到 louzai1 依赖的对象名 louzai2需要获取 louzai2 的 bean是 AbstractBeanFactory 的方法。正式获取 louzai2 的 bean。到这里第一层套娃基本结束因为 louzai1 依赖 louzai2下面我们进入第二层套娃。2.3 第二层获取 louzai2 的 bean从 doGetBean()到 doResolveDependency()和第一层的逻辑完全一样找到 louzai2 依赖的对象名 louzai1。前面的流程全部省略直接到 doResolveDependency()。正式获取 louzai1 的 bean。到这里第二层套娃结束因为 louzai2 依赖 louzai1所以我们进入第三层套娃。2.4 第三层获取 louzai1 的 bean在第一层和第二层中我们每次都会从 getSingleton() 获取对象但是由于之前没有初始化 louzai1 和 louzai2 的三级缓存所以获取对象为空。敲重点敲重点敲重点到了第三层由于第三级缓存有 louzai1 数据这里使用三级缓存中的工厂为 louzai1 创建一个代理对象塞入二级缓存。这里就拿到了 louzai1 的代理对象解决了 louzai2 的依赖关系返回到第二层。2.5 返回第二层返回第二层后louzai2 初始化结束这里就结束了么二级缓存的数据啥时候会给到一级呢甭着急看这里还记得在 doGetBean() 中我们会通过 createBean() 创建一个 louzai2 的 bean当 louzai2 的 bean 创建成功后我们会执行 getSingleton()它会对 louzai2 的结果进行处理。我们进入 getSingleton()会看到下面这个方法。这里就是处理 louzai2 的 一、二级缓存的逻辑将二级缓存清除放入一级缓存。2.6 返回第一层同 2.5louzai1 初始化完毕后会把 louzai1 的二级缓存清除将对象放入一级缓存。到这里所有的流程结束我们返回 louzai1 对象。3. 原理深度解读3.1 什么要有 3 级缓存 这是一道非常经典的面试题前面已经告诉大家详细的执行流程包括源码解读但是没有告诉大家为什么要用 3 级缓存这里是重点敲黑板我们先说“一级缓存”的作用变量命名为 singletonObjects结构是 MapString, Object它就是一个单例池将初始化好的对象放到里面给其它线程使用如果没有第一级缓存程序不能保证 Spring 的单例属性。“二级缓存”先放放我们直接看“三级缓存”的作用变量命名为 singletonFactories结构是 MapString, ObjectFactory?Map 的 Value 是一个对象的代理工厂所以“三级缓存”的作用其实就是用来存放对象的代理工厂。那这个对象的代理工厂有什么作用呢我先给出答案它的主要作用是存放半成品的单例 Bean目的是为了“打破循环”可能大家还是不太懂这里我再稍微解释一下。我们回到文章开头的例子创建 A 对象时会把实例化的 A 对象存入“三级缓存”这个 A 其实是个半成品因为没有完成 A 的依赖属性 B 的注入所以后面当初始化 B 时B 又要去找 A这时就需要从“三级缓存”中拿到这个半成品的 A这里描述其实也不完全准确因为不是直接拿为了让大家好理解我就先这样描述打破循环。那我再问一个问题为什么“三级缓存”不直接存半成品的 A而是要存一个代理工厂呢 答案是因为 AOP。在解释这个问题前我们看一下这个代理工厂的源码让大家有一个更清晰的认识。直接找到创建 A 对象时把实例化的 A 对象存入“三级缓存”的代码直接用前面的两幅截图。下面我们主要看这个对象工厂是如何得到的进入 getEarlyBeanReference() 方法。最后一幅图太重要了我们知道这个对象工厂的作用如果 A 有 AOP就创建一个代理对象如果 A 没有 AOP就返回原对象。那“二级缓存”的作用就清楚了就是用来存放对象工厂生成的对象这个对象可能是原对象也可能是个代理对象。我再问一个问题为什么要这样设计呢把二级缓存干掉不行么 我们继续往下看。3.2 能干掉第 2 级缓存么 Service public class A { Autowired private B b; Autowired private C c; public void test1() { } } Service public class B { Autowired private A a; public void test2() { } } Service public class C { Autowired private A a; public void test3() { } }根据上面的套娃逻辑A 需要找 B 和 C但是 B 需要找 AC 也需要找 A。假如 A 需要进行 AOP因为代理对象每次都是生成不同的对象如果干掉第二级缓存只有第一、三级缓存B 找到 A 时直接通过三级缓存的工厂的代理对象生成对象 A1。C 找到 A 时直接通过三级缓存的工厂的代理对象生成对象 A2。看到问题没你通过 A 的工厂的代理对象生成了两个不同的对象 A1 和 A2所以为了避免这种问题的出现我们搞个二级缓存把 A1 存下来下次再获取时直接从二级缓存获取无需再生成新的代理对象。所以“二级缓存”的目的是为了避免因为 AOP 创建多个对象其中存储的是半成品的 AOP 的单例 bean。如果没有 AOP 的话我们其实只要 1、3 级缓存就可以满足要求。4. 写在最后我们再回顾一下 3 级缓存的作用一级缓存为“Spring 的单例属性”而生就是个单例池用来存放已经初始化完成的单例 Bean二级缓存为“解决 AOP”而生存放的是半成品的 AOP 的单例 Bean三级缓存为“打破循环”而生存放的是生成半成品单例 Bean 的工厂方法。如果你能理解上面我说的三条恭喜你你对 Spring 的循环依赖理解得非常透彻关于循环依赖的知识其实还有因为篇幅原因我就不再写了这篇文章的重点一方面是告诉大家循环依赖的核心原理另一方面是让大家自己去 debug 代码跑跑流程挺有意思的。可能有同学会问 “楼哥你之前是不是经常看源码然后这个流程你是不是 debug 了很久”我之前其实没怎么看过开源代码这个流程前期理论知识看了 2.5 个小时然后 debug 4.5 小时就基本全部走通了最难的地方就是三层套娃稍微有些绕。这里也简单说一下我看源码的心得需要掌握基本的设计模式看源码前最好能找一些理论知识先看看学会读英文注释不会的话就百度翻译debug 时要克制自己不要陷入无用的细节这个最重要。其中最难的是第 4 步因为很多同学看 Spring 源码每看一个方法就想多研究研究这样很容易被绕进去了这个要学会克制有大局观并能分辨哪里是核心逻辑至于如何分辨可以在网上先找些资料如果没有的话就只能多看代码了。

相关新闻

AIGC检测与优化工具千笔的技术原理与应用

AIGC检测与优化工具千笔的技术原理与应用

1. 项目背景与核心价值"千笔专业降AIGC智能体"这个工具名称本身就揭示了它的核心定位——这是一款面向学术场景的AIGC内容检测与优化工具。在当前AI生成内容(AIGC)爆发的时代,教育领域面临着前所未有的挑战:如何区分学生…

2026/10/25 16:58:33 阅读更多 →
Rust边缘运行时MicroDuck:从架构到OTA升级治理的完整评测

Rust边缘运行时MicroDuck:从架构到OTA升级治理的完整评测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 11:18:01 阅读更多 →
RT-Thread嵌入式AI工业质检实战:低代码落地22fps端侧缺陷检测

RT-Thread嵌入式AI工业质检实战:低代码落地22fps端侧缺陷检测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 11:18:10 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/26 12:00:21 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/25 22:36:10 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/26 4:52:12 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/26 12:00:21 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/26 5:00:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/26 3:16:46 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 12:00:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 13:21:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 8:34:37 阅读更多 →