Spring Bean的创建过程与三级缓存的关系详解

news2025/4/15 21:29:34

以下以 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 是原始对象,属性 bnull
    • 未存入任何缓存

步骤 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 是原始对象,属性 anull
    • 未存入任何缓存

步骤 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
  • 升级缓存:将早期引用 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(可能是代理对象)存入 singletonObjects,并清除二、三级缓存。
    registerSingleton("a", a); // 或 proxyA
    

4. 三级缓存的交互总结

Bean A Bean B 一级缓存 二级缓存 三级缓存 存入 ObjectFactory (A) 触发创建 Bean B 存入 ObjectFactory (B) 触发获取 Bean A 通过 ObjectFactory 生成早期引用 升级到二级缓存 (A 的早期引用) 完成初始化,存入 Bean B 完成初始化,存入 Bean A Bean A Bean B 一级缓存 二级缓存 三级缓存

5. 关键设计思想

  1. 提前暴露半成品:允许未初始化的 Bean 被其他 Bean 引用,打破循环依赖的死锁。
  2. 动态代理兼容:通过 ObjectFactory 延迟生成早期引用,确保 AOP 代理逻辑正确执行。
  3. 缓存层级隔离
    • 一级缓存:存放完全可用的 Bean(安全)。
    • 二级缓存:临时存放早期引用(加速依赖查找)。
    • 三级缓存:存放工厂,支持动态扩展(如代理)。

6. Bean的创建是否都需要经历三级缓存


1. 必须经历三级缓存的场景

条件:当 Bean 是单例(Singleton)且 存在循环依赖(通过属性注入)时,Spring 会通过三级缓存机制解决依赖问题。此时 Bean 的创建流程如下:

graph LR
    A[实例化 Bean] --> B[注册 ObjectFactory 到三级缓存]
    B --> C[填充属性(触发循环依赖)]
    C --> D[从三级缓存升级到二级缓存]
    D --> E[完成初始化后存入一级缓存]
示例:Bean A 和 Bean B 互相依赖
  • 步骤
    1. 创建 Bean A 时,实例化后注册 ObjectFactory 到三级缓存。
    2. 注入 Bean B 时触发 B 的创建。
    3. 创建 Bean B 时,实例化后注册 ObjectFactory 到三级缓存。
    4. 注入 Bean A 时,通过三级缓存获取 A 的早期引用(升级到二级缓存)。
    5. Bean B 完成初始化后存入一级缓存。
    6. Bean A 完成初始化后存入一级缓存。

2. 不经历三级缓存的场景
场景 1:无循环依赖的普通 Bean

条件:Bean 是单例,且 没有循环依赖(如 Bean C 无依赖或依赖已存在的 Bean)。

流程

实例化 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 如何通过三级缓存协作,在保证单例完整性的前提下,优雅解决循环依赖问题。

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

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

相关文章

泛型的二三事

泛型(Generics)是Java语言的一个重要特性,它允许在定义类、接口和方法时使用类型参数(Type Parameters),从而实现类型安全的代码重用。泛型在Java 5中被引入,极大地增强了代码的灵活性和安全性。…

编程思想——FP、OOP、FRP、AOP、IOC、DI、MVC、DTO、DAO

个人简介 👀个人主页: 前端杂货铺 🙋‍♂️学习方向: 主攻前端方向,正逐渐往全干发展 📃个人状态: 研发工程师,现效力于中国工业软件事业 🚀人生格言: 积跬步…

【区块链安全 | 第三十九篇】合约审计之delegatecall(一)

文章目录 外部调用函数calldelegatecallcall 与 delegatecall 的区别示例部署后初始状态调用B.testCall()函数调用B.testDelegatecall()函数区别总结漏洞代码代码审计攻击代码攻击原理解析攻击流程修复建议审计思路外部调用函数 在 Solidity 中,常见的两种底层外部函数调用方…

linux多线(进)程编程——(6)共享内存

前言 话说进程君的儿子经过父亲点播后就开始闭关,它想要开发出一种全新的传音神通。他想,如果两个人的大脑生长到了一起,那不是就可以直接知道对方在想什么了吗,这样不是可以避免通过语言传递照成的浪费吗? 下面就是它…

信息安全管理与评估2021年国赛正式卷答案截图以及十套国赛卷

2021年全国职业院校技能大赛高职组 “信息安全管理与评估”赛项 任务书1 赛项时间 共计X小时。 赛项信息 赛项内容 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 第一阶段 平台搭建与安全设备配置防护 任务1 网络平台搭建 任务2 网络安全设备配置与防护 第二…

高并发秒杀系统设计:关键技术解析与典型陷阱规避

电商、在线票务等众多互联网业务场景中,高并发秒杀活动屡见不鲜。这类活动往往在短时间内会涌入海量的用户请求,对系统架构的性能、稳定性和可用性提出了极高的挑战。曾经,高并发秒杀架构设计让许多开发者望而生畏,然而&#xff0…

微信小程序实战案例 - 餐馆点餐系统 阶段 2 – 购物车

阶段 2 – 购物车(超详细版) 目标 把“加入购物车”做成 全局状态,任何页面都能读写在本地 持久化(关闭小程序后购物车仍在)新建 购物车页:数量增减、总价实时计算、去结算入口打 Git Tag v2.0‑cart 1. …

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 协议。它可以让开发者将应用程序及其依赖项打包到一个轻量级、可移植的容器中,并在任…

【websocket】使用案例( ​JSR 356 标准)

目录 一、JSR 356方式:简单示例 1、引入依赖 2、注册端点扫描器 3、编写通过注解处理生命周期和消息 4、细节解读 5、总结 二、聊天室案例 方案流程 1、引入依赖 2、注册端点扫描器 3、编写一个配置类,读取httpsession 4、编写通过注解处理生…

IS-IS中特殊字段——OL过载

文章目录 OL 过载位 🏡作者主页:点击! 🤖Datacom专栏:点击! ⏰️创作时间:2025年04月13日20点12分 OL 过载位 路由过载 使用 IS-IS 的过载标记来标识过载状态 对设备设置过载标记后&#xff…

【时频谱分析】快速谱峭度

算法配置页面,也可以一键导出结果数据 报表自定义绘制 获取和下载【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(默认&#xf…

微软Exchange管理中心全球范围宕机

微软已确认Exchange管理中心(Exchange Admin Center,EAC)发生全球性服务中断,导致管理员无法访问关键管理工具。该故障被标记为关键服务事件(编号EX1051697),对依赖Exchange Online的企业造成广…

基于Qt的串口通信工具

程序介绍 该程序是一个基于Qt的串口通信工具,专用于ESP8266 WiFi模块的AT指令配置与调试。主要功能包括: 1. 核心功能 串口通信:支持串口开关、参数配置(波特率、数据位、停止位、校验位)及数据收发。 AT指令操作&a…

html简易实现推箱子小游戏原理(易上手)

实现效果 使用方向键移动&#xff0c;将橙色箱子推到绿色目标区域&#xff08;黑色块为墙&#xff0c;白色块为可通过区域&#xff0c;蓝球为小人&#xff09; 实现过程 <!DOCTYPE html> <html> <head><title>推箱子小游戏</title><style&g…

字符串与栈和队列-算法小结

字符串 双指针 反转字符串(双指针) 力扣题目链接 void reverseString(vector<char>& s) {for (int i 0, j s.size() - 1; i < s.size()/2; i, j--) {swap(s[i],s[j]);} }反转字符串II 力扣题目链接 遍历字符串的过程中&#xff0c;只要让 i (2 * k)&#…

类似东郊到家的上门按摩预约服务系统小程序APP源码全开源

&#x1f525; 为什么上门按摩正在席卷全国&#xff1f; 万亿蓝海市场爆发 2024年中国按摩市场规模突破8000亿&#xff0c;上门服务增速达65% 90后成消费主力&#xff0c;**72%**白领每月至少使用1次上门按摩&#xff08;数据来源&#xff1a;艾媒咨询&#xff09; 传统痛点…

Python | 在Pandas中按照中值对箱形图排序

箱形图是可视化数据分布的强大工具&#xff0c;因为它们提供了对数据集内的散布、四分位数和离群值的洞察。然而&#xff0c;当处理多个组或类别时&#xff0c;通过特定的测量&#xff08;如中位数&#xff09;对箱形图进行排序可以提高清晰度并有助于揭示模式。在本文中&#…