当前位置: 首页 > news >正文

对于SpringBoot的三层缓存的思考

文章目录

  • 前言
  • 一、SpringBoot具体解决了什么循环依赖?
    • 1.1 非构造函数注入
    • 1.2 构造函数注入
    • 1.3 构造函数和非构造函数混合注入
      • 1.3.1 不能解决的案例
      • 1.3.2 可以解决的案例
      • 1.3.3 总结
  • 二、如何解决的?
    • 2.1 依赖注入时机
    • 2.2 三层缓存
      • 2.2.1 三个重要的缓存容器
      • 2.2.2 核心方法:getSingleton()
    • 2.3 说明
  • 三、三级缓存到底解决了什么问题
    • 3.1 第三级缓存的特殊性
    • 3.2 二级缓存能不能解决循环依赖问题
    • 3.3 三级缓存实际解决的问题


前言

在阅读 Spring Boot 源码的过程中,我对 createBean 方法中那段广为人知的逻辑——“三级缓存解决循环依赖”——产生了浓厚的兴趣。我花了相当多的时间去研究源码,并参考了许多网上的解析与讨论。

关于为什么要设计三级缓存,不同的说法层出不穷,网上的博客质量参差不齐:有人认为这是出于性能优化的考虑,有人认为是为了支持 AOP 代理的提前暴露,也有人认为两级缓存无法完全应对循环依赖的问题。起初我也在这些观点之间反复权衡,但随着理解的深入,我找到了自己的答案。

我认为,Spring Boot 之所以采用三级缓存的根本目的,并不在于性能或特定功能的支持,而在于在遵循 Bean 生命周期语义的前提下,允许在循环依赖的特殊场景中适度突破这一语义。

换句话说,三级缓存主要维护了 Spring 对 Bean 创建过程的规范性。

一、SpringBoot具体解决了什么循环依赖?

Spring Boot中,Bean 之间的依赖可以通过多种方式注入,例如:

  • 构造函数注入(Constructor Injection)

  • 字段注入(@Autowired)

  • Setter 方法注入

  • 接口回调注入(如 BeanFactoryAware、ApplicationContextAware 等)

但并不是所有注入方式都可能导致循环依赖,也不是所有循环依赖 Spring 都能“救回来”。三级缓存机制所能解决的,实际上只是**“单例 Bean 之间通过属性注入(字段或 Setter)产生的循环依赖”**。

这里的构造函数注入最为特殊,因为它的注入时机是最早的,所以这里我将它们分为构造函数注入非构造函数注入


1.1 非构造函数注入

@Component
@Data
public class CycleDependenceTestA {@Autowiredpublic CycleDependenceTestB cycleDependenceTestB;public CycleDependenceTestA() {}
}
@Component
@Data
public class CycleDependenceTestB {@Autowiredpublic CycleDependenceTestA cycleDependenceTestA;public CycleDependenceTestB() {}
}

容器正常启动,循环依赖问题被解决。


1.2 构造函数注入

构造函数注入比较特殊,因为它的注入时机是最早的。

@Component
@Data
public class CycleDependenceTestA {public CycleDependenceTestB cycleDependenceTestB;public CycleDependenceTestA(CycleDependenceTestB cycleDependenceTestB) {this.cycleDependenceTestB = cycleDependenceTestB;}
}
@Component
@Data
public class CycleDependenceTestB {public CycleDependenceTestA cycleDependenceTestA;public CycleDependenceTestB(CycleDependenceTestA cycleDependenceTestA) {this.cycleDependenceTestA = cycleDependenceTestA;}
}

结果会报错:

Description:The dependencies of some of the beans in the application context form a cycle:┌─────┐
|  cycleDependenceTestA defined in file [E:\Java space\codes\SpringBootDemo\target\classes\org\example\springbootdemo\beans\CycleDependenceTestA.class]
↑     ↓
|  cycleDependenceTestB defined in file [E:\Java space\codes\SpringBootDemo\target\classes\org\example\springbootdemo\beans\CycleDependenceTestB.class]
└─────┘Action:Despite circular references being allowed, the dependency cycle between beans could not be broken. Update your application to remove the dependency cycle.Process finished with exit code 1

SpringBoot无法解决这种情况,原因很简单,往后看了解三层缓存原理后,自然会明白。

1.3 构造函数和非构造函数混合注入

这里的情况最为特殊和有趣,大家可以猜一猜,这种情况下的循环依赖的问题能否解决呢?
我在这里告诉大家答案:50%概率可以解决,和注入的先后顺序有关系。

1.3.1 不能解决的案例

@Component
@Data
public class CycleDependenceTestA {public CycleDependenceTestB cycleDependenceTestB;public CycleDependenceTestA(CycleDependenceTestB cycleDependenceTestB) {this.cycleDependenceTestB = cycleDependenceTestB;}
}
@Component
@Data
public class CycleDependenceTestB {@Autowiredpublic CycleDependenceTestA cycleDependenceTestA;public CycleDependenceTestB() {}}

结果是报了循环依赖的错误的:

Description:The dependencies of some of the beans in the application context form a cycle:┌─────┐
|  cycleDependenceTestA defined in file [E:\Java space\codes\SpringBootDemo\target\classes\org\example\springbootdemo\beans\CycleDependenceTestA.class]
↑     ↓
|  cycleDependenceTestB (field public org.example.springbootdemo.beans.CycleDependenceTestA org.example.springbootdemo.beans.CycleDependenceTestB.cycleDependenceTestA)
└─────┘Action:Despite circular references being allowed, the dependency cycle between beans could not be broken. Update your application to remove the dependency cycle.Process finished with exit code 1

1.3.2 可以解决的案例

@Component
@Data
public class CycleDependenceTestA {@Autowiredpublic CycleDependenceTestB cycleDependenceTestB;public CycleDependenceTestA() {}
}
@Component
@Data
public class CycleDependenceTestB {public CycleDependenceTestA cycleDependenceTestA;public CycleDependenceTestB(CycleDependenceTestA cycleDependenceTestA) {this.cycleDependenceTestA = cycleDependenceTestA;}
}

容器正常启动

1.3.3 总结

上述两个案例中,一个循环依赖被解决,另一个无法被解决,代码区别是什么?其实就是执行顺序的问题。至于为什么,先卖个关子,如果你看过底层源码,了解Bean的生命周期,创建流程,自然会理解。请往下看。

二、如何解决的?

2.1 依赖注入时机

核心代码在AbstractAutowireCapableBeanFactory的doCreateBean方法中。

// 核心逻辑:AbstractAutowireCapableBeanFactory#doCreateBean
// 1. 创建 Bean 实例(构造函数注入在此阶段完成)
instanceWrapper = createBeanInstance(beanName, mbd, args);// 2. 暴露早期引用,将用于生成代理的 ObjectFactory 放入第三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));// 3. 属性依赖注入(此阶段执行 @Autowired、@Value 等注解逻辑)
//   关键处理器:AutowiredAnnotationBeanPostProcessor
populateBean(beanName, mbd, instanceWrapper);// 4. Bean 初始化(执行初始化回调与 AOP 代理创建等逻辑)
//   关键处理器:AbstractAutoProxyCreator 及其他 BeanPostProcessor
exposedObject = initializeBean(beanName, exposedObject, mbd);

2.2 三层缓存

Spring 在解决循环依赖问题时,核心逻辑位于 DefaultSingletonBeanRegistry 类中。

2.2.1 三个重要的缓存容器

它们共同构成了所谓的三级缓存机制:

// 一级缓存:存放完全初始化完成的单例 Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);// 二级缓存:存放提前暴露但尚未完全初始化的 Bean 实例
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);// 三级缓存:存放可以生成 Bean 早期引用的工厂(通常是用于生成代理的 ObjectFactory)
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);

2.2.2 核心方法:getSingleton()

// 源码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lock.Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {if (!this.singletonLock.tryLock()) {// Avoid early singleton inference outside of original creation thread.return null;}try {// Consistent creation of early reference within full singleton lock.singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();// Singleton could have been added or removed in the meantime.if (this.singletonFactories.remove(beanName) != null) {this.earlySingletonObjects.put(beanName, singletonObject);}else {singletonObject = this.singletonObjects.get(beanName);}}}}}finally {this.singletonLock.unlock();}}}return singletonObject;}

核心逻辑如下:

  1. 先从一级缓存中找
    Object singletonObject = this.singletonObjects.get(beanName);
    
  2. 一级缓存找不到,再从二级缓存找
    singletonObject = this.earlySingletonObjects.get(beanName);
    
  3. 二级缓存也没有,则从三级缓存取出工厂并生成早期引用
    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
    if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.singletonFactories.remove(beanName);this.earlySingletonObjects.put(beanName, singletonObject);
    }
    

2.3 说明

上面的代码展示了 Spring 解决循环依赖的核心实现逻辑。为了更清晰地理解,我们可以先回到 Bean 的创建生命周期。

一个 Bean 从创建到最终可用,大致会经历三个阶段:
实例化 → 属性注入 → 初始化。
只有当 Bean 完成初始化后,才能被认为是一个“完整可用”的 Bean。

循环依赖问题的关键在于:
当 Bean A 依赖 Bean B,而 Bean B 又依赖 Bean A 时,如果严格按照生命周期顺序执行,那么双方都会在“属性注入阶段”卡住——因为此时彼此都还没有完成创建。

Spring 的解决思路是:

在实例化完成但尚未初始化之前,提前暴露一个可以引用的 Bean 对象,供其他 Bean 使用。

换句话说,即使一个 Bean 还没完全准备好(属性未注入、后置处理器未执行),Spring 也允许通过三级缓存机制,将它的“早期引用”暴露出去。这样,另一个 Bean 在注入时就能拿到一个有效的引用,从而打破循环依赖的僵局。


三、三级缓存到底解决了什么问题

3.1 第三级缓存的特殊性

private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);

第三级缓存相较于前两级缓存更为特殊——它保存的不是 Bean 实例本身,而是一个 ObjectFactory 对象,也就是一个可执行的回调函数。
要理解这种设计的意义,我们需要看看 Spring 在向第三级缓存注册时,究竟放入了什么逻辑:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// class AbstractAutowireCapableBeanFactory
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);}}return exposedObject;
}

Spring 会遍历所有实现了 SmartInstantiationAwareBeanPostProcessor 接口的后置处理器,让它们有机会提前介入 Bean 的引用创建过程。

其中最典型的后置处理器就是 AbstractAutoProxyCreator,它正是 Spring AOP 的底层核心之一。

// class AbstractAutoProxyCreator
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = getCacheKey(bean.getClass(), beanName);this.earlyBeanReferences.put(cacheKey, bean);return wrapIfNecessary(bean, beanName, cacheKey);
}

这里的 wrapIfNecessary() 方法会判断当前 Bean 是否需要被 AOP 切面增强:

如果需要增强,就在此时创建代理对象并返回;

如果不需要,则直接返回原始对象。

换句话说,这一步可能会生成 Bean 的代理对象,并将其作为“早期引用”暴露出去。

第三级缓存的存在意义就在于:当出现循环依赖时,如果另一个 Bean 需要当前 Bean 的引用,Spring 能通过第三级缓存中的 ObjectFactory 提前触发代理逻辑,返回正确的引用(包括可能的代理对象),从而保证依赖注入和最终 Bean 一致性。

3.2 二级缓存能不能解决循环依赖问题

事实上,从循环依赖本身的角度来看,二级缓存也完全可以解决问题。
因为三级缓存的核心功能,是在 Bean 初始化之前允许返回一个“早期引用”。
如果我们直接在实例化之后、属性注入之前,将早期引用放入二级缓存,同样能够实现循环依赖的解环。

假设我们对 Spring 的逻辑稍作改造,不使用三级缓存,而是直接在实例化后生成早期引用并放入二级缓存:

// AbstractAutowireCapableBeanFactory(伪代码改造)
if (earlySingletonExposure) {if (logger.isTraceEnabled()) {logger.trace("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}addEarlySingletonObjects(beanName, getEarlyBeanReference(beanName, mbd, bean));
}

在这段伪代码中,Spring 不再保存 ObjectFactory,而是直接调用
getEarlyBeanReference() 获取早期引用(包括可能的 AOP 代理对象),
然后立即将其放入二级缓存 earlySingletonObjects 中,供其他 Bean 在依赖注入时使用。

从表面上看,这样确实可以达到同样的效果:

循环依赖照样被解决;

AOP 代理也能提前生成;

性能上没有任何差别(甚至更直接)。

3.3 三级缓存实际解决的问题

Spring 通过三级缓存设计了一个延迟生成早期引用的机制:它既能解决循环依赖,又能在大多数情况下保持 Bean 生命周期和 AOP 代理逻辑的语义一致性。

具体来说:

在 正常创建流程 中,Bean 会严格遵循生命周期:实例化 → 属性注入 → 初始化 → 后置处理器(生成 AOP 代理)。

三级缓存的作用是为 循环依赖 这种突发情况 提供一个弹性通道:当 Bean 之间存在循环依赖时,Spring 可以通过三级缓存提前生成早期引用(可能是代理对象),从而打破循环依赖的僵局。

换句话说,三级缓存是一种 “在必要时允许突破生命周期规范的机制”,保证循环依赖能够被安全解决,同时不会影响绝大多数 Bean 的正常创建流程。

http://www.dtcms.com/a/457128.html

相关文章:

  • Spring Boot 中使用 Caffeine 缓存详解与案例
  • 零基础网站建设教学服务wordpress点击图片不显示不出来
  • 大模型原理与实践:第六章-大模型训练流程实践_第1部分-模型预训练(Trainer、DeepSeed)
  • 分析DAO组织如何重构开发者协作关系
  • Kotlin:现代编程语言的革新者
  • 济南集团网站建设价格专门做汽车配件的外贸网站
  • word属性解释
  • 大连网站关键字优化电脑网页游戏平台
  • AIoT(人工智能物联网):融合范式下的技术演进、系统架构与产业变革
  • 关于旅行的网站怎样做wordpress首页文章带图
  • 2.GDI图形绘制基础
  • 公司做网站需要什么资料百度关键词搜索查询
  • 母婴微网站设计规划中国最新新闻头条
  • 2025-10-08 Python 标准库 9——内置类型:其他
  • Oracle实战:相同批次下D5_D10最新数据整合为一行的3种方案
  • 私人精品货源网站有哪些php网站开发前端
  • 金融 - 搭建 图谱挖掘工作流 调研
  • 图像分割关于DualSeg,FFM和CFM的论文学习
  • Spring的 `@Import`注解 笔记251008
  • 【玩泰山派】4、制作ubuntu镜像-(6)使用鲁班猫的sdk去制作镜像
  • 长兴县住房和城乡建设局网站我想看黄牌
  • 深入理解 Reactor 反应堆模式:高性能网络编程的核心
  • php做小公司网站用什么框架医药招商网站大全免费
  • 从 0 到 1 掌控云原生部署:Java 项目的 Docker 容器化与 K8s 集群实战指南
  • 哪里可以做足球网站虚拟主机 2个网站
  • 建设银行的英语网站首页dede导入wordpress
  • 支付宝小程序 MAU 增长新路径:生态工具与流量闭环的协同实战
  • C++ 成员初始化列表
  • 三门县住房和城乡建设规划局网站商业网站是怎么做的
  • Spring Security 最简配置完全指南-从入门到精通前后端分离安全配置