Java 王者修炼手册【Spring 篇 - 循环依赖 & 三级缓存】:Bean 创建流程 + 循环依赖 + 三级缓存原理 大揭秘

大家好,我是程序员强子。
最近在后台收到小伙伴留言,说想让我分析一下 循环依赖到底是怎么回事,缓存又为啥设计成三级,两级不行嘛~ 等等这些问题。
那今天我们就盘一下这些问题,争取一次性拿下~
来看下今天的知识点:
循环依赖的 Bean 创建流程 :涉及的核心类 、具体创建流程
三级缓存的设计思想:选择三级缓存而非两级的具体原因、三级缓存的核心设计思路、以及每一级缓存各自承担的核心作用
来不及解释了,发车了~
Bean 创建流程
以 BeanA(单例,Setter 注入 BeanB) 和 BeanB(单例,Setter 注入 BeanA) 的循环依赖为例。
前置条件
BeanA 和 BeanB 均为单例(Spring 默认作用域);
依赖注入方式为 Setter 注入 ;
容器底层由 DefaultListableBeanFactory 实现,三级缓存逻辑由 DefaultSingletonBeanRegistry 维护
那会不会跟强子一样有疑问:
DefaultListableBeanFactory 和 **DefaultSingletonBeanRegistry **到底是什么?有什么作用呢?
核心类总结
DefaultListableBeanFactory 是什么?
首先 回顾一下 BeanFactory ,是 IoC 容器的最顶层核心接口,定义了 IoC 容器的基础规范
仅提供最基础的 Bean 操作能力 getBean()(获取 Bean 实例)containsBean()(判断 Bean 是否存在)isSingleton()(判断 Bean 是否为单例)
是 最小功能集,后续所有实现都是围绕这接口扩展
DefaultListableBeanFactory 是 BeanFactory 体系中最完整的实现类,是容器的 基础骨架
实现了 BeanFactory 接口
实现了 ListableBeanFactory(支持批量获取 Bean)
实现了 AutowireCapableBeanFactory(支持自动装配)
实现了 BeanDefinitionRegistry(支持注册 Bean 定义)
具备 Bean 定义注册,实例化,依赖注入,自动装配 能力
我们 比较熟悉的 ApplicationContext,底层依赖 DefaultListableBeanFactory
ApplicationContext 的具体实现 ClassPathXmlApplicationContext、AnnotationConfigApplicationContext
内部都会创建 DefaultListableBeanFactory 实例 作为 bean 服务接口~
所有 Bean 的创建、管理、缓存 操作最终都委托给 DefaultListableBeanFactory 执行
所以 得出一个结论: DefaultListableBeanFactory 全权管理 注册 Bean、处理循环依赖、管理三级缓存 !
那 DefaultSingletonBeanRegistry 是什么?
是 Spring 中单例 Bean 缓存机制的核心实现类
定义三级缓存的存储结构 singletonObjects(一级缓存):存储完全初始化完成的单例 Bean,是最终可直接使用的 Bean 缓存;earlySingletonObjects(二级缓存):存储早期暴露的 Bean 实例(已实例化但未完成属性填充和初始化的 Bean),用于解决循环依赖时的临时引用;singletonFactories(三级缓存):存储 Bean 的对象工厂(ObjectFactory),用于在需要时创建早期 Bean 实例(AOP 代理对象或原始对象)
提供缓存操作核心方法单例 Bean 的获取(getSingleton())注册(addSingleton())早期对象获取(getEarlyBeanReference())
Spring 框架的类设计严格遵循单一职责原则 ,将不同功能解耦到不同类中
DefaultSingletonBeanRegistry 的核心职责:专注于单例 Bean 的缓存管理、生命周期跟踪与循环依赖处理
DefaultListableBeanFactory 的核心职责:专注于 Bean 定义注册、加载,Bean 实例化、依赖注入、自动装配等容器核心调度逻辑
这两个类的调用链路如下:
DefaultListableBeanFactory → AbstractBeanFactory(调用 getSingleton())→ DefaultSingletonBeanRegistry(执行三级缓存的查找 / 注册 / 升级)
好了,万事俱备只欠东风,跟着强子的脚步,进一步分析 具体创建流程~
具体创建流程
步骤 1:触发 BeanA 的创建
容器调用 AbstractBeanFactory.getBean("beanA")
最终委托 DefaultSingletonBeanRegistry.getSingleton 处理:
检查一级缓存(singletonObjects):无 BeanA 实例;
检查二级缓存(earlySingletonObjects):无 BeanA 早期引用;
检查三级缓存(singletonFactories):无 BeanA 的工厂对象;
将 BeanA 标记为 创建中(加入 singletonsCurrentlyInCreation 集合,防止重复创建并检测循环依赖)
singletonsCurrentlyInCreation 集合有什么作用?
最核心的作用:标记 Bean 的创建中状态,识别循环依赖当 开始创建 单例 Bean 时,会先将 名称加入当 Bean 完成初始化(实例化、属性填充、初始化方法执行等)后,再将其从集合中移除
防止单例 Bean 的重复实例化多个线程同时尝试获取同一个未创建的 Bean 时,第一个线程会将 Bean 加入集合并开始创建后续线程检查到 Bean 已在集合中,会等待第一个线程创建完成,而非重新实例化
步骤 2:实例化 BeanA
AbstractAutowireCapableBeanFactory.createBeanInstance()
通过反射(Constructor.newInstance())创建 BeanA 的原始实例
仅完成对象实例化,属性为默认值
步骤 3:暴露 BeanA 的早期引用到三级缓存
将生成 BeanA 早期引用的 ObjectFactory 放入三级缓存(singletonFactories)
此时,ObjectFactory 是一个工厂接口 ,作用是延迟创建 / 获取对象
当调用 ObjectFactory.getObject() 时
需要代理 → 工厂返回代理对象
不需要代理 → 工厂返回原始对象
步骤 4:填充 BeanA 的属性
AbstractAutowireCapableBeanFactory.populateBean 处理属性注入
发现 BeanA 依赖 BeanB,调用 getBean("beanB")触发 BeanB 的创建流程
步骤 5:触发 BeanB 的创建
重复步骤 1-3
检查三级缓存,无 BeanB 相关记录,标记 BeanB 为 创建中
实例化 BeanB 的原始实例
将 BeanB 的 ObjectFactory 放入三级缓存
步骤 6:填充 BeanB 的属性
发现 BeanB 依赖 BeanA,调用 getBean("beanA")获取 BeanA
步骤 7:获取 BeanA 的早期引用
再次调用 getSingleton("beanA"):
检查一级缓存:无;
检查二级缓存:无;
检查三级缓存:存在 BeanA 的 ObjectFactory;
调用 ObjectFactory.getObject()生成 BeanA 的早期引用(若需代理则为代理对象,否则为原始对象);
将 BeanA 的早期引用从三级缓存移至二级缓存,并移除三级缓存中的 BeanA
返回 BeanA 的早期引用给 BeanB。
为什么执行了 ObjectFactory.getObject()生成的产物是放到二级缓存而不是一级缓存?
二级缓存的存在是为了隔离早期对象与完整 Bean ,而一级缓存必须保证存储的是最终可用的单例 Bean
而 ObjectFactory.getObject()生成的产物 未完成:属性填充与初始化 ,属于半成品,因此不能放到一级缓存中。
步骤 8:完成 BeanB 的初始化与注册
BeanB 拿到 BeanA 的早期引用,完成属性填充;
执行 BeanB 的初始化逻辑:调用 @PostConstruct 注解方法;执行 InitializingBean.afterPropertiesSet();调用自定义初始化方法(init-method);
注册 BeanB 到一级缓存,将 BeanB 完整实例放入 singletonObjects;
移除 BeanB 在二级 / 三级缓存的记录,标记 BeanB 为 “创建完成”(从 singletonsCurrentlyInCreation 移除)。
步骤 9:完成 BeanA 的属性填充与初始化
回到 BeanA 的属性填充步骤,此时 BeanB 已存在于一级缓存,直接注入 BeanB 的完整实例;
执行 BeanA 的初始化逻辑(同 BeanB 的初始化步骤);
检查 BeanA 是否存在早期引用(二级缓存中)若 BeanA 无需代理:直接使用原始实例;若 BeanA 需代理:早期引用已为代理对象,直接复用;
注册 BeanA 到一级缓存;
移除 BeanA 在二级缓存的记录,标记 BeanA 为 创建完成(从 singletonsCurrentlyInCreation 移除)
大家会不会杠一下,为什么一定要三级缓存呢?二级行不行?
那就跟强子来深入分析一下为啥一定要三级,少一级都不行的原因~
为什么一定要三级缓存?
第一步:最初的需求
最初的需求,保证单例 Bean 全局唯一,且仅对外暴露 完全初始化完成 的实例(实例化 + 属性填充 + 初始化 + AOP 代理)
此时最朴素的设计是用一个 Map 存储:
单例 Bean 的核心价值是 复用,一个 Map 完全满足 存和 取 的基础需求;
只有当 Bean 完全初始化后,才放入这个 Map,避免对外暴露不完整的 Bean
这个时候,问题出现了:
当出现循环依赖(如 A 依赖 B、B 依赖 A)时
A 在初始化过程中需要 B,B 初始化过程中又需要 A
此时两个 Bean 都未进入 singletonObjects,陷入 你等我、我等你 的循环中,死锁了~
第二步:解决基础循环依赖
设计者意识到:核心矛盾是 Bean 未完全初始化,但依赖方需要它的引用。
既然无法提前完成初始化,能否在 Bean 实例化后、初始化前,先把它的 早期引用 暴露出来?
于是新增第二个 Map 存储早期引用
对单例 Bean,实例化(调用构造器)后,此时 Bean 已经有了内存地址(引用),只是属性未填充、未初始化;
把这个早期引用存入 earlySingletonObjects,当依赖方需要时,直接从这里取引用,打破循环依赖的死锁
新问题来了:如果 Bean 需要 AOP 代理 。。
早期引用是原始 Bean 实例,但最终需要的是代理实例
若依赖方 B 拿到的是原始实例,而 A 最终会变成代理实例,
就会导致 依赖不一致(B 依赖原始 A,容器里最终是代理 A)
第三步:处理代理 Bean 的一致性
设计者进一步思考:不能直接存储早期实例,而应存储 “能生成最终实例或者代理实例的工厂
当依赖方需要时,通过工厂动态生成 Bean 的早期引用(需要代理则生成代理,不需要则返回原始)
保证依赖方拿到的引用与最终 Bean 一致
于是新增第三个 Map 存储工厂:
ObjectFactory 是一个延迟执行的工厂接口,其 getObject()方法会在真正需要引用时才调用;
当 Bean 实例化后,不直接存早期引用,而是存一个工厂:() -> getEarlyBeanReference(beanName, mbd, bean);
当依赖方需要该 Bean 时,调用工厂的 getObject(),此时会触发 AOP 代理逻辑(由 SmartInstantiationAwareBeanPostProcessor 处理),生成代理后的早期引用;
生成后的早期引用会从三级缓存移到二级缓存(避免重复生成),保证全局唯一
安全校验
新增 singletonsCurrentlyInCreation 集合
在集合里面,就代表正在创建,标记为 正在创建的 Bean
防止构造器注入循环依赖导致的死循环
三级缓存总结
一级缓存:守护 单例 Bean 最终一致性 的底线;
二级缓存:临时存储 可用的早期引用,打破循环依赖;
三级缓存:解决 代理 Bean 早期引用的一致性 问题,兼顾延迟代理的设计原则
Spring 无法解决的循环依赖的场景
构造器注入导致的循环依赖
代码 demo
问题解析
Spring 启动时会尝试创建 BeanA,调用 BeanA(BeanB)构造器时需要先获取 BeanB;
创建 BeanB 时,调用 BeanB(BeanA)构造器又需要先获取 BeanA。
此时两个 Bean 都处于 构造器实例化阶段,还未执行到 实例化后注册三级缓存 的步骤
而三级缓存是实例化后才注册 ObjectFactory
因此 无法暴露早期引用,
最终触发 BeanCurrentlyInCreationException 异常,循环依赖无法解决
解决方案
方案 1 :改为 Setter 注入
Setter 注入允许 Bean 先实例化(触发三级缓存注册),再填充依赖,符合 Spring 循环依赖解决方案的流程
方案 2:构造器注入 + @Lazy 延迟加载
原理:@Lazy 在构造器注入时生成依赖 Bean 的代理对象,避免实例化阶段的循环等待。
方案 3:使用 ObjectFactory 延迟获取依赖
ObjectFactory 封装依赖获取逻辑,延迟到实际使用时再触发依赖 Bean 的创建
总结
今天,终于把困扰很久的 循环依赖 和 三级缓存 问题 搞清楚了~
主要讲了 循环依赖的 Bean 创建流程 以及 三级缓存的设计思想,
还补充了 一些 核心类的总结,使得我们更好的了解 创建流程的内容~
感谢小伙伴给强子留言,也希望解 小伙伴的 疑问 ~
熟练度刷不停,知识点吃透稳,下期接着练~
版权声明: 本文为 InfoQ 作者【DonaldCen】的原创文章。
原文链接:【http://xie.infoq.cn/article/1e58be0e871344e165ff3f6c3】。文章转载请联系作者。







评论