对于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;}
核心逻辑如下:
- 先从一级缓存中找
Object singletonObject = this.singletonObjects.get(beanName);
- 一级缓存找不到,再从二级缓存找
singletonObject = this.earlySingletonObjects.get(beanName);
- 二级缓存也没有,则从三级缓存取出工厂并生成早期引用
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 的正常创建流程。