Spring Bean的创建过程与三级缓存的关系详解
以下以 Bean A 和 Bean B 互相依赖为例,结合源码和流程图,详细说明 Bean 的创建过程与三级缓存的交互。
1. Bean 的完整生命周期(简化版)
2. 三级缓存的作用
| 缓存名称 | 存储内容 | 目的 |
|---|---|---|
singletonObjects | 完全初始化好的 Bean(成品) | 直接对外提供可用的 Bean |
earlySingletonObjects | 未初始化完成的 Bean(半成品,早期引用) | 临时存储,供其他 Bean 提前引用,避免循环依赖卡死 |
singletonFactories | 创建 Bean 的 ObjectFactory(工厂对象) | 延迟生成早期引用(支持 AOP 代理等动态扩展) |
3. 详细流程(以 Bean A → Bean B → Bean A 循环依赖为例)
步骤 1:开始创建 Bean A
- 入口:调用
getBean("a")。 - 阶段:实例化(
createBeanInstance())。// 反射调用无参构造函数创建原始对象 A a = new A(); - 此时状态:
a是原始对象,属性b为null。- 未存入任何缓存。
步骤 2:提前暴露 Bean A 的工厂
- 操作:将 Bean A 的
ObjectFactory存入 三级缓存(singletonFactories)。// AbstractAutowireCapableBeanFactory.doCreateBean() addSingletonFactory("a", () -> getEarlyBeanReference("a", mbd, a)); - 目的:允许其他 Bean(如 Bean B)在依赖注入时,通过工厂获取 Bean A 的早期引用(可能是代理对象)。
步骤 3:填充 Bean A 的属性(发现依赖 Bean B)
- 操作:调用
populateBean("a"),检测到a依赖b。 - 触发:调用
getBean("b")创建 Bean B。
步骤 4:开始创建 Bean B
- 入口:调用
getBean("b")。 - 阶段:实例化(
createBeanInstance())。B b = new B(); // 反射创建原始对象 - 此时状态:
b是原始对象,属性a为null。- 未存入任何缓存。
步骤 5:提前暴露 Bean B 的工厂
- 操作:将 Bean B 的
ObjectFactory存入 三级缓存。addSingletonFactory("b", () -> getEarlyBeanReference("b", mbd, b));
步骤 6:填充 Bean B 的属性(发现依赖 Bean A)
- 操作:调用
populateBean("b"),检测到b依赖a。 - 触发:再次调用
getBean("a")。
步骤 7:再次获取 Bean A(解决循环依赖)
- 检查一级缓存:
singletonObjects中没有 Bean A。 - 检查二级缓存:
earlySingletonObjects中没有 Bean A。 - 检查三级缓存:找到 Bean A 的
ObjectFactory。 - 操作:调用
ObjectFactory.getObject(),实际执行getEarlyBeanReference()。// 生成早期引用(可能是代理对象) Object earlyA = getEarlyBeanReference("a", mbd, a);- 若 Bean A 需要 AOP 代理:此时生成代理对象(如
A$$EnhancerBySpringCGLIB)。 - 若无需代理:直接返回原始对象
a。
- 若 Bean A 需要 AOP 代理:此时生成代理对象(如
- 升级缓存:将早期引用
earlyA存入 二级缓存(earlySingletonObjects),并清除三级缓存中的工厂。this.earlySingletonObjects.put("a", earlyA); this.singletonFactories.remove("a"); - 结果:Bean B 获得 Bean A 的早期引用(
earlyA),完成属性注入b.setA(earlyA)。
步骤 8:完成 Bean B 的初始化
- 初始化:执行
initializeBean("b")(包括@PostConstruct、AOP 代理等)。 - 存入一级缓存:将完全初始化的 Bean B 存入
singletonObjects。registerSingleton("b", b);
步骤 9:回到 Bean A 的属性填充
- 操作:Bean A 获得完全初始化的 Bean B(
b),完成属性注入a.setB(b)。
步骤 10:完成 Bean A 的初始化
- 初始化:执行
initializeBean("a")。- 若 Bean A 需要代理:此时生成代理对象
proxyA(覆盖原始对象a)。 - 若无需代理:直接使用原始对象
a。
- 若 Bean A 需要代理:此时生成代理对象
- 存入一级缓存:将最终 Bean A(可能是代理对象)存入
singletonObjects,并清除二、三级缓存。registerSingleton("a", a); // 或 proxyA
4. 三级缓存的交互总结
5. 关键设计思想
- 提前暴露半成品:允许未初始化的 Bean 被其他 Bean 引用,打破循环依赖的死锁。
- 动态代理兼容:通过
ObjectFactory延迟生成早期引用,确保 AOP 代理逻辑正确执行。 - 缓存层级隔离:
- 一级缓存:存放完全可用的 Bean(安全)。
- 二级缓存:临时存放早期引用(加速依赖查找)。
- 三级缓存:存放工厂,支持动态扩展(如代理)。
6. Bean的创建是否都需要经历三级缓存
1. 必须经历三级缓存的场景
条件:当 Bean 是单例(Singleton)且 存在循环依赖(通过属性注入)时,Spring 会通过三级缓存机制解决依赖问题。此时 Bean 的创建流程如下:
graph LRA[实例化 Bean] --> B[注册 ObjectFactory 到三级缓存]B --> C[填充属性(触发循环依赖)]C --> D[从三级缓存升级到二级缓存]D --> E[完成初始化后存入一级缓存]
示例:Bean A 和 Bean B 互相依赖
- 步骤:
- 创建 Bean A 时,实例化后注册
ObjectFactory到三级缓存。 - 注入 Bean B 时触发 B 的创建。
- 创建 Bean B 时,实例化后注册
ObjectFactory到三级缓存。 - 注入 Bean A 时,通过三级缓存获取 A 的早期引用(升级到二级缓存)。
- Bean B 完成初始化后存入一级缓存。
- Bean A 完成初始化后存入一级缓存。
- 创建 Bean A 时,实例化后注册
2. 不经历三级缓存的场景
场景 1:无循环依赖的普通 Bean
条件:Bean 是单例,且 没有循环依赖(如 Bean C 无依赖或依赖已存在的 Bean)。
流程:
关键点:
- 不需要提前暴露早期引用,直接跳过三级缓存。
- 例如:Bean C 依赖的 Bean D 已经在一级缓存中,则直接注入 D,无需触发缓存升级。
场景 2:构造器注入的循环依赖
条件:Bean 使用 构造器注入 导致循环依赖。
结果:
Spring 无法解决构造器注入的循环依赖,直接抛出 BeanCurrentlyInCreationException。
原因:
- 构造器注入需在实例化阶段完成依赖注入,此时 Bean 尚未创建完成,无法提前暴露到三级缓存。
场景 3:原型(Prototype)作用域的 Bean
条件:Bean 的作用域为 prototype。
结果:
Spring 不缓存原型 Bean,每次请求都创建新实例,因此:
- 不涉及三级缓存。
- 循环依赖直接报错(原型 Bean 不支持循环依赖)。
场景 4:无需代理的 Bean
条件:Bean 不需要 AOP 代理,且无循环依赖。
流程:
直接通过反射创建原始对象,完成初始化后存入一级缓存,全程不涉及三级缓存。
3. 三级缓存的触发条件总结
| 条件 | 是否触发三级缓存 | 示例场景 |
|---|---|---|
| 单例 + 属性注入 + 循环依赖 | ✔️ | Bean A → Bean B → Bean A |
| 单例 + 无循环依赖 | ❌ | 普通 Service 类 |
| 单例 + 构造器注入循环依赖 | ❌(直接报错) | 构造器注入导致循环依赖 |
| 原型作用域 Bean | ❌ | 每次请求新实例 |
| 需要代理但无循环依赖 | ❌ | 独立 Bean 使用 @Transactional |
4. 源码验证
(1) 三级缓存的注册逻辑
在 AbstractAutowireCapableBeanFactory.doCreateBean() 中,只有满足以下条件时才会注册 ObjectFactory:
// 条件:单例 + 允许循环引用 + Bean 正在创建中
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
(2) 无循环依赖时的跳过逻辑
若 Bean 无循环依赖,则不会触发从三级缓存获取早期引用的代码:
// DefaultSingletonBeanRegistry.getSingleton()
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {// 无循环依赖时,不会进入此分支if (isSingletonCurrentlyInCreation(beanName)) {// 从三级缓存获取早期引用的逻辑...}
}
总结
- 必须经历三级缓存:仅当单例 Bean 存在属性注入的循环依赖时。
- 不经历三级缓存:
- 无循环依赖的单例 Bean。
- 构造器注入的循环依赖(直接报错)。
- 原型作用域 Bean。
- 不需要代理的普通 Bean。
7. 常见问题解答
Q1:为什么需要三级缓存?二级缓存不够吗?
- 三级缓存的核心价值:解耦 Bean 的创建 和 代理的生成。
- 如果只有二级缓存:
代理逻辑需在实例化后立即执行(违反 Spring 的设计原则,代理应在初始化阶段完成)。 - 三级缓存通过
ObjectFactory延迟代理生成,确保代理逻辑在正确的时机执行。
- 如果只有二级缓存:
Q2:构造器注入为何无法解决循环依赖?
- 根本原因:构造器注入需在实例化阶段完成依赖注入,而实例化尚未完成时无法提前暴露对象(三级缓存机制无法介入)。
Q3:为什么二级缓存叫 earlySingletonObjects?
- 它存储的是“早期单例对象”(尚未完成初始化),与一级缓存的“完全体单例”区分。
通过以上流程,可以清晰理解 Spring 如何通过三级缓存协作,在保证单例完整性的前提下,优雅解决循环依赖问题。
相关文章:
Spring Bean的创建过程与三级缓存的关系详解
以下以 Bean A 和 Bean B 互相依赖为例,结合源码和流程图,详细说明 Bean 的创建过程与三级缓存的交互。 1. Bean 的完整生命周期(简化版) #mermaid-svg-uwqaB5dgOFDQ97Yd {font-family:"trebuchet ms",verdana,arial,sa…...
IDEA 调用 Generate 生成 Getter/Setter 快捷键
快捷键不会用? 快捷键:AltInsert 全选键:CtrlA IDEA 调用 Generate 生成 Getter/Setter 快捷键 - 爱吃西瓜的番茄酱 - 博客园...
泛型的二三事
泛型(Generics)是Java语言的一个重要特性,它允许在定义类、接口和方法时使用类型参数(Type Parameters),从而实现类型安全的代码重用。泛型在Java 5中被引入,极大地增强了代码的灵活性和安全性。…...
编程思想——FP、OOP、FRP、AOP、IOC、DI、MVC、DTO、DAO
个人简介 👀个人主页: 前端杂货铺 🙋♂️学习方向: 主攻前端方向,正逐渐往全干发展 📃个人状态: 研发工程师,现效力于中国工业软件事业 🚀人生格言: 积跬步…...
实现一个动态验证码生成器:Canvas与JavaScript的完美结合
验证码(CAPTCHA)是现代网站中常见的安全机制,用于区分人类用户和自动化程序。本文将详细介绍如何使用HTML5 Canvas和JavaScript创建一个美观且功能完整的验证码生成器。 一、核心功能概述 这个验证码生成器具有以下特点: 随机生…...
python中 “with” 关键字的取舍问题
自动管理资源(自动关闭文件) 当你使用 with 打开文件时,文件会在 with 代码块结束后自动关闭,无论是否发生异常。这意味着你不需要显式地调用 f.close() 来关闭文件 示例: with open("words.txt", "r…...
【区块链安全 | 第三十九篇】合约审计之delegatecall(一)
文章目录 外部调用函数calldelegatecallcall 与 delegatecall 的区别示例部署后初始状态调用B.testCall()函数调用B.testDelegatecall()函数区别总结漏洞代码代码审计攻击代码攻击原理解析攻击流程修复建议审计思路外部调用函数 在 Solidity 中,常见的两种底层外部函数调用方…...
Nginx部署spa单页面的小bug
没部署过,都是给后端干的,自己尝试部署了一个下午终于成功了 我遇到的最大的bug是进入后只有首页正常显示 其他页面全是404,于是问问问才知道,需要这个 location / { try_files $uri $uri/ /index.html; } 让…...
linux多线(进)程编程——(6)共享内存
前言 话说进程君的儿子经过父亲点播后就开始闭关,它想要开发出一种全新的传音神通。他想,如果两个人的大脑生长到了一起,那不是就可以直接知道对方在想什么了吗,这样不是可以避免通过语言传递照成的浪费吗? 下面就是它…...
【愚公系列】《Python网络爬虫从入门到精通》050-搭建 Scrapy 爬虫框架
🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…...
信息安全管理与评估2021年国赛正式卷答案截图以及十套国赛卷
2021年全国职业院校技能大赛高职组 “信息安全管理与评估”赛项 任务书1 赛项时间 共计X小时。 赛项信息 赛项内容 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 第一阶段 平台搭建与安全设备配置防护 任务1 网络平台搭建 任务2 网络安全设备配置与防护 第二…...
讲解贪心算法
贪心算法是一种常用的算法思想,其在解决问题时每一步都做出在当前状态下看起来最优的选择,从而希望最终能够获得全局最优解。C作为一种流行的编程语言,可以很好地应用于贪心算法的实现。下面我们来讲一篇关于C贪心算法的文章。 目录 贪心算法…...
高并发秒杀系统设计:关键技术解析与典型陷阱规避
电商、在线票务等众多互联网业务场景中,高并发秒杀活动屡见不鲜。这类活动往往在短时间内会涌入海量的用户请求,对系统架构的性能、稳定性和可用性提出了极高的挑战。曾经,高并发秒杀架构设计让许多开发者望而生畏,然而࿰…...
微信小程序实战案例 - 餐馆点餐系统 阶段 2 – 购物车
阶段 2 – 购物车(超详细版) 目标 把“加入购物车”做成 全局状态,任何页面都能读写在本地 持久化(关闭小程序后购物车仍在)新建 购物车页:数量增减、总价实时计算、去结算入口打 Git Tag v2.0‑cart 1. …...
Qt 元对象系统探秘:从 Q_OBJECT 到反射编程的魔法之旅
背景说明:Qt 背后的「魔法引擎」 如果你曾用 Qt 写过信号槽,或是在设计器里拖过控件改属性,一定对这个框架的“动态性”印象深刻: 无需手动调用,信号能自动连接到槽函数;无需编译重启,界面上修…...
sql 向Java的映射
优化建议,可以在SQL中控制它的类型 在 MyBatis 中,如果返回值类型设置为 java.util.Map,默认情况下可以返回 多行多列的数据...
Visual Studio未能加载相应的Package包弹窗报错
环境介绍: visulal studio 2019 问题描述: 起因:安装vs扩展插件后,重新打开Visual Studio,报了一些列如下的弹窗错误,即使选择不继续显示该错误,再次打开后任然报错; 解决思路&am…...
【HD-RK3576-PI】Docker搭建与使用
硬件:HD-RK3576-PI 软件:Linux6.1Ubuntu22.04 1.Docker 简介 Docker 是一个开源的应用容器引擎,基于 Go 语言开发,遵循 Apache 2.0 协议。它可以让开发者将应用程序及其依赖项打包到一个轻量级、可移植的容器中,并在任…...
C语言实现用户管理系统
以下是一个简单的C语言用户管理系统示例,它实现了用户信息的添加、删除、修改和查询功能。代码中包含了详细的注释和解释,帮助你理解每个部分的作用。 #include <stdio.h> #include <stdlib.h> #include <string.h>#define MAX_USERS…...
【websocket】使用案例( JSR 356 标准)
目录 一、JSR 356方式:简单示例 1、引入依赖 2、注册端点扫描器 3、编写通过注解处理生命周期和消息 4、细节解读 5、总结 二、聊天室案例 方案流程 1、引入依赖 2、注册端点扫描器 3、编写一个配置类,读取httpsession 4、编写通过注解处理生…...
tcpdump`是一个非常强大的命令行工具,用于在网络上捕获并分析数据包
通过 tcpdump,你可以抓取网络流量,诊断网络问题,或分析通信协议的细节。下面是如何在 Linux 上使用 tcpdump 进行抓包的详细步骤。 1. 安装 tcpdump 在大多数 Linux 发行版中,tcpdump 是默认安装的。如果没有安装,可…...
IS-IS中特殊字段——OL过载
文章目录 OL 过载位 🏡作者主页:点击! 🤖Datacom专栏:点击! ⏰️创作时间:2025年04月13日20点12分 OL 过载位 路由过载 使用 IS-IS 的过载标记来标识过载状态 对设备设置过载标记后ÿ…...
【时频谱分析】快速谱峭度
算法配置页面,也可以一键导出结果数据 报表自定义绘制 获取和下载【PHM学习软件PHM源码】的方式 获取方式:Docshttps://jcn362s9p4t8.feishu.cn/wiki/A0NXwPxY3ie1cGkOy08cru6vnvc...
Spring Boot 支持的内嵌服务器(Tomcat、Jetty、Undertow、Netty(用于 WebFlux 响应式应用))详解
Spring Boot 支持的内嵌服务器详解 1. 支持的内嵌服务器 Spring Boot 默认支持以下内嵌服务器: Tomcat(默认)JettyUndertowNetty(用于 WebFlux 响应式应用) 2. 各服务器使用示例 (1) Tomcat(默认…...
微软Exchange管理中心全球范围宕机
微软已确认Exchange管理中心(Exchange Admin Center,EAC)发生全球性服务中断,导致管理员无法访问关键管理工具。该故障被标记为关键服务事件(编号EX1051697),对依赖Exchange Online的企业造成广…...
基于AI的Web应用防火墙(AppWall)实战:漏洞拦截与威胁情报集成
摘要:针对Web应用面临的OWASP、CVE等漏洞攻击,本文结合群联AI云防护系统的AppWall模块,详解AI规则双引擎的防御原理,并提供漏洞拦截配置与威胁情报集成代码示例。 一、Web应用安全挑战与AppWall优势 传统WAF依赖规则库更新滞后&a…...
基于Qt的串口通信工具
程序介绍 该程序是一个基于Qt的串口通信工具,专用于ESP8266 WiFi模块的AT指令配置与调试。主要功能包括: 1. 核心功能 串口通信:支持串口开关、参数配置(波特率、数据位、停止位、校验位)及数据收发。 AT指令操作&a…...
CSS 字体学习笔记
在网页设计中,字体的使用对于提升用户体验和页面美观性至关重要。CSS 提供了一系列字体属性,用于控制文本的显示效果。以下是对 CSS 字体属性的详细学习笔记。 一、字体系列(font-family) 1. 字体系列的分类 在 CSS 中…...
《MySQL是怎样运行的》总结笔记
内容太多,主要总结一些自己认为重要的,另外太基础常见可能不会总结上。 字符集和比较规则 MySQL会通过把字符串编码后再进行比较大小并排序,有一些很早的字符集可能会不支持中文,比如ASCII、ISO 8859-1,现在最常用的…...
力扣每日打卡 1922. 统计好数字的数目 (中等)
力扣 1922. 统计好数字的数目 中等 前言一、题目内容二、解题方法1. 暴力解法(会超时,此法不通)2. 快速幂运算3. 组合计数的思维逻辑分析组合计数的推导例子分析思维小结论 4.官方题解4.1 方法一:快速幂 三、快速幂运算快速幂运算…...
