java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot三级缓存

关于SpringBoot的三级缓存的思考问题小结

作者:GEMjay

文章解析SpringBoot三级缓存机制,指出其核心在于通过提前暴露早期引用解决单例Bean属性注入的循环依赖,支持AOP代理,但构造函数注入无法解决,本质是为特殊场景提供生命周期弹性通道,本文给大家介绍对于SpringBoot的三层缓存的思考,感兴趣的朋友一起看看吧

前言

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

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

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

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

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

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

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

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

1.1 非构造函数注入

@Component
@Data
public class CycleDependenceTestA {
    @Autowired
    public CycleDependenceTestB cycleDependenceTestB;
    public CycleDependenceTestA() {}
}
@Component
@Data
public class CycleDependenceTestB {
    @Autowired
    public 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 {
    @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 可以解决的案例

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

// 核心逻辑: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 的正常创建流程。

到此这篇关于对于SpringBoot的三层缓存的思考的文章就介绍到这了,更多相关SpringBoot三层缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文