对于SpringBoot的三层缓存的思考

文章目录

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

java 复制代码
@Component
@Data
public class CycleDependenceTestA {
    @Autowired
    public CycleDependenceTestB cycleDependenceTestB;
    public CycleDependenceTestA() {}
}
@Component
@Data
public class CycleDependenceTestB {
    @Autowired
    public CycleDependenceTestA cycleDependenceTestA;
    public CycleDependenceTestB() {}
}

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


1.2 构造函数注入

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

java 复制代码
@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 不能解决的案例

java 复制代码
@Component
@Data
public class CycleDependenceTestA {

    public CycleDependenceTestB cycleDependenceTestB;

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

    @Autowired
    public 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 可以解决的案例

java 复制代码
@Component
@Data
public class CycleDependenceTestA {

    @Autowired
    public 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方法中。

java 复制代码
// 核心逻辑: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 三个重要的缓存容器

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

java 复制代码
// 一级缓存:存放完全初始化完成的单例 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()

java 复制代码
// 源码
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. 先从一级缓存中找

    java 复制代码
    Object singletonObject = this.singletonObjects.get(beanName);
  2. 一级缓存找不到,再从二级缓存找

    java 复制代码
    singletonObject = this.earlySingletonObjects.get(beanName);
  3. 二级缓存也没有,则从三级缓存取出工厂并生成早期引用

    java 复制代码
    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 第三级缓存的特殊性

java 复制代码
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);

第三级缓存相较于前两级缓存更为特殊------它保存的不是 Bean 实例本身,而是一个 ObjectFactory 对象,也就是一个可执行的回调函数。

要理解这种设计的意义,我们需要看看 Spring 在向第三级缓存注册时,究竟放入了什么逻辑:

java 复制代码
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
java 复制代码
// 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 的底层核心之一。

java 复制代码
// 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 的逻辑稍作改造,不使用三级缓存,而是直接在实例化后生成早期引用并放入二级缓存:

java 复制代码
// 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 的正常创建流程。

相关推荐
泉城老铁3 小时前
导出大量数据时如何优化内存使用?SXSSFWorkbook的具体实现方法是什么?
spring boot·后端·excel
泉城老铁4 小时前
springboot实现对接poi 导出excel折线图
java·spring boot·后端
稚辉君.MCA_P8_Java4 小时前
kafka解决了什么问题?mmap 和sendfile
java·spring boot·分布式·kafka·kubernetes
Lisonseekpan6 小时前
Spring Boot 中使用 Caffeine 缓存详解与案例
java·spring boot·后端·spring·缓存
Terio_my7 小时前
Spring Boot Web环境测试配置
spring boot
汤姆yu8 小时前
2025版基于springboot的美食食品商城系统
spring boot·后端·美食
kfepiza8 小时前
Spring 如何解决循环依赖 笔记251008
java·spring boot·spring
Arva .10 小时前
Spring Boot 配置文件
java·spring boot·后端
IT_Octopus10 小时前
https私人证书 PKIX path building failed 报错解决
java·spring boot·网络协议·https