Spring的三级缓存详解
目录
1、什么是三级缓存
2、三级缓存详解
Bean实例化前
属性赋值/注入前
初始化后
总结
3、怎么解决的循环依赖
4、不用三级缓存不行吗
5、总结
一、什么是三级缓存
就是在Bean生成流程中保存Bean对象三种形态的三个Map集合,如下:

用来解决什么问题 ?
这个大家应该熟知了,就是循环依赖
什么是循环依赖 ?
就像下面这样,AService 中注入了BService ,而BService 中又注入了AService ,这就是循环依赖

这几个问题我们结合源码来一起看一下:
三级缓存分别在什么地方产生的?
三级缓存是怎么解决循环依赖的?
一定需要三级缓存吗?二级缓存不行?
二、三级缓存详解
不管你了不了解源码,我们先看一下Bean的生成流程,看看三级缓存是在什么地方有调用,就三个地方:
1、Bean实例化前会先查询缓存,判断Bean是否已经存在
2、Bean属性赋值前会先向三级缓存中放入一个lambda表达式,该表达式执行时会获取一个半成品Bean放入二级缓存并删除三级缓存
3、Bean初始化完成后将完整的Bean放入一级缓存,同时清空二、三级缓存
接下来我们一个一个看!

1、Bean实例化前
AbstractBeanFactory.doGetBean
Bean实例化前会从缓存里面获取Bean,防止重复实例化

DefaultSingletonBeanRegistry.getSingleton(String beanName, boolean allowEarlyReference)
我们看看这个获取的方法逻辑:
a、从一级缓存获取,获取到了,则返回
b、从二级缓存获取,获取到了,则返回
c、从三级缓存获取,获取到了,则执行三级缓存中的lambda表达式,将结果放入二级缓存,清除三级缓存

2、属性赋值/注入前
AbstractAutowireCapableBeanFactory.doCreateBean

DefaultSingletonBeanRegistry.addSingletonFactory
这里就是将一个lambda表达式放入了三级缓存,我们需要去看一下这个表达式是干什么的!!

AbstractAutowireCapableBeanFactory.getEarlyBeanReference 该方法在属性赋值之前、初始化之前执行
重点,该方法说白了就是会判断该Bean是否需要被动态代理,两种返回结果:
不需要代理,返回未属性注入、未初始化的半成品Bean
需要代理,返回未属性注入、未初始化的半成品Bean的代理对象

注意:这里只是把lambda表达式放入了三级缓存,如果不从三级缓存中获取,这个表达式是不执行的,一旦执行了,就会把半成品Bean 或 者半成品Bean的代理对象放入二级缓存中了
3、初始化后
AbstractBeanFactory.doGetBean
执行流程,sharedInstance = getSingleton(beanName, new ObjectFactory() --> singletonObject = singletonFactory.getObject() --> createBean方法 --> 又返回到singletonObject = singletonFactory.getObject()

DefaultSingletonBeanRegistry.getSingleton(beanName, singletonFactory)
这个方法与上面那个不一样,重载了

DefaultSingletonBeanRegistry.addSingleton

总结
整个过程就三个地方跟缓存有关,我们假设现在要实例化A这个Bean,看看缓存是怎么变化的:
1、实例化前,获取缓存判断(三个缓存中肯定没有A,获取为null,进入实例化流程)
2、实例化完成,属性注入前(往三级缓存中放入了一个lambda表达式,一、二级为null)
3、初始化完成(将A这个Bean放入一级缓存,清除二、三级缓存)
以上则是单个Bean生成过程中缓存的变化!!
三、怎么解决的循环依赖
上面我们把Bean流程中利用缓存的三个重要的点都找出来了,也分析了会带来什么变化,接下来看看是怎么解决的循环依赖,我们看个图就懂了:
以A注入B,B注入A为例:
A属性注入前就把lambda表达式放入了第三级缓存,所以B再注入A的时候会从第三级缓存中找到A的lambda表达式并执行,然后将半成品Bean放入第二级缓存,所以此时B注入的只是半成品的A对象,然后B将完整对象返回给A注入,A继续初始化,完成创建。

从上述看第三级缓存是用来提前暴露Bean对象引用的,所以解决了循环依赖,但是第二级缓存的这个半成品Bean对象干嘛的呢 ?
假设A同时注入了B和C,B和C又都注入了A,这时A注入B,实例化B的过程和上述是一样的,但随后还会注入C,那这个C在注入A的时候还会有第三级缓存用吗?没了吧,所以它就只能用第二级缓存的半成品Bean对象了,同样也是引用而已
四、不用三级缓存不行吗
可能很多小伙伴得到的答案就是不行,而且答案是因为不确定这个Bean是不是代理对象,所以搞了个lambda表达式?答案真的是这样吗 ?
那为什么要设计成三级缓存,而不是两级呢?比如说,如果只有二级缓存的话,可能会有什么问题?可能和AOP代理有关。因为如果Bean被AOP代理,那么在生成代理对象的时候,需要确保在注入的时候使用的是最终的代理对象。三级缓存中的工厂可以处理这种情况,根据需要返回原始对象或者代理对象,而二级缓存可能无法处理这种情况,导致多次代理 或者 代理不一致的问题。
示例1
比如,当Bean A被AOP代理时,在创建A的原始对象后,会有一个工厂放入三级缓存。当其他Bean B需要引用A时,会通过这个工厂获取代理对象,然后将这个代理对象放到二级缓存中。这样后续再需要A的时候,可以直接从二级缓存拿到代理对象,而不用再通过工厂创建,保证单例。如果没有三级缓存,只有二级的话,可能在处理代理对象的时候会遇到问题,比如多次调用工厂或者无法正确生成代理。
另外,三级缓存的存在可能还涉及到性能优化。比如,避免在不需要的时候过早地创建代理对象,只有在有循环依赖的情况下才会通过工厂来生成代理,这样在无循环依赖的情况下,可以延迟代理的创建,提高效率。
不过,可能有些情况下二级缓存也可以解决循环依赖的问题,但为什么Spring选择了三级呢?比如,假设只有一级和二级缓存,当Bean A创建时,实例化后放到二级缓存,然后填充属性时发现需要Bean B。Bean B在创建时同样需要Bean A,这时候从二级缓存拿到A的早期对象,完成B的创建,然后A继续填充。这样看起来可能可以解决循环依赖。但问题在于,如果A需要被代理的话,这时候在二级缓存中的A可能还是原始对象,而不是代理后的对象。而Spring的AOP通常是在Bean初始化后处理的,比如通过BeanPostProcessor。这时候,如果在填充属性的时候引用了原始对象,但最终Bean却是代理对象,就会导致不一致,因为其他Bean引用的可能是原始对象,而不是代理后的,这样AOP的增强就会失效。
所以,为了解决这个问题,Spring引入了三级缓存,在需要提前暴露Bean的时候,不是直接暴露实例,而是通过一个工厂来生成。这个工厂可以处理需要代理的情况,返回代理后的对象。这样,当存在循环依赖时,通过三级缓存中的工厂,可以生成正确的代理对象,并将其放入二级缓存,后续直接从二级缓存获取,而不用每次都通过工厂创建,同时保证所有依赖方都使用同一个代理对象。
总结一下,三级缓存主要是为了解决循环依赖中存在的代理问题,确保即使存在AOP代理,也能正确地处理依赖注入,避免出现不一致的引用。而二级缓存可能无法处理这种情况,导致代理对象被多次创建或引用的对象不正确。因此,Spring设计三级缓存是为了更细粒度地控制Bean的创建过程,处理各种复杂的依赖场景,尤其是涉及AOP的情况。

五、总结
一级缓存:用于存储被完整创建了的bean。也就是完成了初始化之后,可以直接被其他对象使用的bean。
二级缓存:用于存储半成品的Bean。也就是刚实例化但是还没有进行初始化的Bean
三级缓存:三级缓存存储的是工厂对象(lambda表达式)。工厂对象可以产生Bean对象提前暴露的引用(半成品的Bean或者半成品的代理Bean对象),执行这个lambda表达式,就会将引用放入二级缓存中
经过以上的分析,现在应该懂了吧:
循环依赖是否一定需要三级缓存来解决? 不一定,但三级缓存会更合适,风险更小
二级缓存能否解决循环依赖? 可以,但风险比三级缓存更大
第二级缓存用来干嘛的? 存放半成品的引用,可能产生多对象循环依赖,第三级缓存产生引用后,后续的就可以直接注入该引用
多例、构造器注入为什么不能解决循环依赖 ?
1、多例(Prototype)Bean为何无法解决循环依赖?
核心原因:作用域的生命周期不同
单例Bean的缓存机制:
Spring通过三级缓存(singletonObjects、earlySingletonObjects、singletonFactories)管理单例Bean的创建过程,提前暴露未完全初始化的Bean实例,以解决循环依赖
多例Bean的特性:
Prototype作用域的Bean每次请求都会创建一个新实例,Spring不缓存多例Bean的实例,因此无法在创建过程中提前暴露一个“半成品”Bean供其他对象使用。
具体场景示例

问题:当尝试获取Bean A或B时,Spring会尝试为每个Bean创建一个新实例,但由于它们相互依赖,每次创建都需要另一个Bean的新实例,导致无限递归,最终抛出 BeanCurrentlyInCreationException
2、构造器注入为何无法解决循环依赖 ?
核心原因:实例化与依赖注入的顺序冲突
构造器注入的时机:
构造器注入发生在Bean的实例化阶段,此时Bean还未完成初始化,无法通过提前暴露引用(如三级缓存)解决循环依赖
属性注入(Setter注入)的时机:
属性注入发生在Bean实例化之后,此时Spring可以通过三级缓存提前暴露一个未完成属性填充的Bean,供其他对象引用
具体场景示例

问题:创建Bean A时,需要先实例化Bean B;而实例化Bean B时,又需要先实例化Bean A。两者互相等待对方完成实例化,导致死锁,最终抛出 BeanCurrentlyInCreationException
Spring的三级缓存详解的更多相关文章
- Spring框架系列(8) - Spring IOC实现原理详解之Bean实例化(生命周期,循环依赖等)
上文,我们看了IOC设计要点和设计结构:以及Spring如何实现将资源配置(以xml配置为例)通过加载,解析,生成BeanDefination并注册到IoC容器中的:容器中存放的是Bean的定义即Be ...
- Spring Data操作Redis详解
Spring Data操作Redis详解 Redis是一种NOSQL数据库,Key-Value形式对数据进行存储,其中数据可以以内存形式存在,也可以持久化到文件系统.Spring data对Redis ...
- Spring各个jar包详解
Spring各jar包详解 spring.jar 是包含有完整发布模块的单个jar 包.但是不包括mock.jar,aspects.jar, spring-portlet.jar, and sprin ...
- Spring Boot 之 Redis详解
Redis是目前业界使用最广泛的内存数据存储. Redis支持丰富的数据结构,同时支持数据持久化. Redis还提供一些类数据库的特性,比如事务,HA,主从库. REmote DIctionary S ...
- Spring IoC @Autowired 注解详解
前言 本系列全部基于 Spring 5.2.2.BUILD-SNAPSHOT 版本.因为 Spring 整个体系太过于庞大,所以只会进行关键部分的源码解析. 我们平时使用 Spring 时,想要 依赖 ...
- Spring IoC 公共注解详解
前言 本系列全部基于 Spring 5.2.2.BUILD-SNAPSHOT 版本.因为 Spring 整个体系太过于庞大,所以只会进行关键部分的源码解析. 什么是公共注解?公共注解就是常见的Java ...
- Spring框架系列(6) - Spring IOC实现原理详解之IOC体系结构设计
在对IoC有了初步的认知后,我们开始对IOC的实现原理进行深入理解.本文将帮助你站在设计者的角度去看IOC最顶层的结构设计.@pdai Spring框架系列(6) - Spring IOC实现原理详解 ...
- Spring框架系列(7) - Spring IOC实现原理详解之IOC初始化流程
上文,我们看了IOC设计要点和设计结构:紧接着这篇,我们可以看下源码的实现了:Spring如何实现将资源配置(以xml配置为例)通过加载,解析,生成BeanDefination并注册到IoC容器中的. ...
- Spring框架系列(9) - Spring AOP实现原理详解之AOP切面的实现
前文,我们分析了Spring IOC的初始化过程和Bean的生命周期等,而Spring AOP也是基于IOC的Bean加载来实现的.本文主要介绍Spring AOP原理解析的切面实现过程(将切面类的所 ...
- elasticSearch+spring 整合 maven依赖详解
摘自:http://www.mayou18.com/detail/nTxPQSyu.html [Elasticsearch基础]elasticSearch+spring 整合 maven依赖详解 Ma ...
随机推荐
- 加速 AI 训推:Lepton AI 如何构建多租户、低延迟云存储平台
Lepton AI 是一款面向开发者的 AI 平台,旨在提供易用.高效且可扩展的基础设施能力.该平台适用于各种训练.推理需求,GPU充足,在保证高性能的同时,能够灵活应对不断变化的工作负载.用户可以快 ...
- 转载:大模型所需 GPU 内存笔记
转载文章:大模型所需 GPU 内存笔记 引言 在运行大型模型时,不仅需要考虑计算能力,还需要关注所用内存和 GPU 的适配情况.这不仅影响 GPU 推理大型模型的能力,还决定了在训练集群中总可用的 G ...
- 使用SOUI4的脚本模块
SOUI4.1提供了全新的lua脚本模块支持,使用这个新版本的脚本模块,可以轻松将所有UI布局及业务逻辑全部使用XML+LUA实现,基本上就是一个超轻型浏览器. SOUI4.0相对于SOUI3最大的区 ...
- 用 C# 写一个 .NET 垃圾回收器(二)
用 C# 写一个 .NET 垃圾回收器(二) 在第一部分中,我们准备了项目,并修复了由 NativeAOT 工具链引起的初始化问题.在本部分,我们将开始实现自己的 GC(垃圾回收器).目前的目标是构建 ...
- nginx适配Overlay以及测试工具
本文分享自天翼云开发者社区<nginx适配Overlay以及测试工具>,作者:pan Overlay与Underlay介绍 Overlay网络和Underlay网络是一组相对概念,Over ...
- JMeter调用python脚本
JMeter调用python脚本 前提 具备python环境 具备jdk环境 一.编写python脚本 python脚本如下: import random # 随机一个 1~100 的随机数 prin ...
- AI 如何重塑劳动力市场:基于 Claude 数据的深度分析
前言 本文翻译自 Anthropic 今天发布的 The Anthropic Economic Index ,经济指数报告,这份报告基于 Claude 的数据对目前的 AI 使用情况做了汇总. 引言 ...
- DispatcherPriority 枚举
DispatcherPriority 枚举 ApplicationIdle 2 枚举值为 2. 在应用程序空闲时处理操作. Background 4 枚举值为 4. 在完成所有其他非空闲操作后处理操作 ...
- JS ellipse 转 PathData
绘制Path function ellipse2path(cx, cy, rx, ry, degree) { //cx cy:圆心 //rx ry:x y 轴长 //degree:度数,顺时针方向为正 ...
- CentOS安装nginx服务器及配置反向代理
以下是在CentOS上安装nginx服务器并配置反向代理的步骤: 更新系统软件包: sudo yum update 安装nginx: sudo yum install nginx 启动nginx服务: ...