關于SpringBoot的三級緩存的思考問題小結
前言
在閱讀 Spring Boot 源碼的過程中,我對 createBean 方法中那段廣為人知的邏輯——“三級緩存解決循環(huán)依賴”——產生了濃厚的興趣。我花了相當多的時間去研究源碼,并參考了許多網上的解析與討論。
關于為什么要設計三級緩存,不同的說法層出不窮,網上的博客質量參差不齊:有人認為這是出于性能優(yōu)化的考慮,有人認為是為了支持 AOP 代理的提前暴露,也有人認為兩級緩存無法完全應對循環(huán)依賴的問題。起初我也在這些觀點之間反復權衡,但隨著理解的深入,我找到了自己的答案。
我認為,Spring Boot 之所以采用三級緩存的根本目的,并不在于性能或特定功能的支持,而在于在遵循 Bean 生命周期語義的前提下,允許在循環(huán)依賴的特殊場景中適度突破這一語義。
換句話說,三級緩存主要維護了 Spring 對 Bean 創(chuàng)建過程的規(guī)范性。
一、SpringBoot具體解決了什么循環(huán)依賴?
Spring Boot中,Bean 之間的依賴可以通過多種方式注入,例如:
- 構造函數注入(Constructor Injection)
- 字段注入(@Autowired)
- Setter 方法注入
- 接口回調注入(如 BeanFactoryAware、ApplicationContextAware 等)
但并不是所有注入方式都可能導致循環(huán)依賴,也不是所有循環(huán)依賴 Spring 都能“救回來”。三級緩存機制所能解決的,實際上只是**“單例 Bean 之間通過屬性注入(字段或 Setter)產生的循環(huán)依賴”**。
這里的構造函數注入最為特殊,因為它的注入時機是最早的,所以這里我將它們分為構造函數注入和非構造函數注入。
1.1 非構造函數注入
@Component
@Data
public class CycleDependenceTestA {
@Autowired
public CycleDependenceTestB cycleDependenceTestB;
public CycleDependenceTestA() {}
}
@Component
@Data
public class CycleDependenceTestB {
@Autowired
public CycleDependenceTestA cycleDependenceTestA;
public CycleDependenceTestB() {}
}容器正常啟動,循環(huán)依賴問題被解決。
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 構造函數和非構造函數混合注入
這里的情況最為特殊和有趣,大家可以猜一猜,這種情況下的循環(huán)依賴的問題能否解決呢?
我在這里告訴大家答案: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() {}
}結果是報了循環(huán)依賴的錯誤的:
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 總結
上述兩個案例中,一個循環(huán)依賴被解決,另一個無法被解決,代碼區(qū)別是什么?其實就是執(zhí)行順序的問題。至于為什么,先賣個關子,如果你看過底層源碼,了解Bean的生命周期,創(chuàng)建流程,自然會理解。請往下看。
二、如何解決的?
2.1 依賴注入時機
核心代碼在AbstractAutowireCapableBeanFactory的doCreateBean方法中。
// 核心邏輯:AbstractAutowireCapableBeanFactory#doCreateBean // 1. 創(chuàng)建 Bean 實例(構造函數注入在此階段完成) instanceWrapper = createBeanInstance(beanName, mbd, args); // 2. 暴露早期引用,將用于生成代理的 ObjectFactory 放入第三級緩存 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); // 3. 屬性依賴注入(此階段執(zhí)行 @Autowired、@Value 等注解邏輯) // 關鍵處理器:AutowiredAnnotationBeanPostProcessor populateBean(beanName, mbd, instanceWrapper); // 4. Bean 初始化(執(zhí)行初始化回調與 AOP 代理創(chuàng)建等邏輯) // 關鍵處理器:AbstractAutoProxyCreator 及其他 BeanPostProcessor exposedObject = initializeBean(beanName, exposedObject, mbd);
2.2 三層緩存
Spring 在解決循環(huán)依賴問題時,核心邏輯位于 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 解決循環(huán)依賴的核心實現邏輯。為了更清晰地理解,我們可以先回到 Bean 的創(chuàng)建生命周期。
一個 Bean 從創(chuàng)建到最終可用,大致會經歷三個階段:
實例化 → 屬性注入 → 初始化。
只有當 Bean 完成初始化后,才能被認為是一個“完整可用”的 Bean。
循環(huán)依賴問題的關鍵在于:
當 Bean A 依賴 Bean B,而 Bean B 又依賴 Bean A 時,如果嚴格按照生命周期順序執(zhí)行,那么雙方都會在“屬性注入階段”卡住——因為此時彼此都還沒有完成創(chuàng)建。
Spring 的解決思路是:
在實例化完成但尚未初始化之前,提前暴露一個可以引用的 Bean 對象,供其他 Bean 使用。
換句話說,即使一個 Bean 還沒完全準備好(屬性未注入、后置處理器未執(zhí)行),Spring 也允許通過三級緩存機制,將它的“早期引用”暴露出去。這樣,另一個 Bean 在注入時就能拿到一個有效的引用,從而打破循環(huán)依賴的僵局。
三、三級緩存到底解決了什么問題
3.1 第三級緩存的特殊性
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);
第三級緩存相較于前兩級緩存更為特殊——它保存的不是 Bean 實例本身,而是一個 ObjectFactory 對象,也就是一個可執(zhí)行的回調函數。
要理解這種設計的意義,我們需要看看 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 的引用創(chuàng)建過程。
其中最典型的后置處理器就是 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 切面增強:
如果需要增強,就在此時創(chuàng)建代理對象并返回;
如果不需要,則直接返回原始對象。
換句話說,這一步可能會生成 Bean 的代理對象,并將其作為“早期引用”暴露出去。
第三級緩存的存在意義就在于:當出現循環(huán)依賴時,如果另一個 Bean 需要當前 Bean 的引用,Spring 能通過第三級緩存中的 ObjectFactory 提前觸發(fā)代理邏輯,返回正確的引用(包括可能的代理對象),從而保證依賴注入和最終 Bean 一致性。
3.2 二級緩存能不能解決循環(huán)依賴問題
事實上,從循環(huán)依賴本身的角度來看,二級緩存也完全可以解決問題。
因為三級緩存的核心功能,是在 Bean 初始化之前允許返回一個“早期引用”。
如果我們直接在實例化之后、屬性注入之前,將早期引用放入二級緩存,同樣能夠實現循環(huán)依賴的解環(huán)。
假設我們對 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 在依賴注入時使用。
從表面上看,這樣確實可以達到同樣的效果:
循環(huán)依賴照樣被解決;
AOP 代理也能提前生成;
性能上沒有任何差別(甚至更直接)。
3.3 三級緩存實際解決的問題
Spring 通過三級緩存設計了一個延遲生成早期引用的機制:它既能解決循環(huán)依賴,又能在大多數情況下保持 Bean 生命周期和 AOP 代理邏輯的語義一致性。
具體來說:
在 正常創(chuàng)建流程 中,Bean 會嚴格遵循生命周期:實例化 → 屬性注入 → 初始化 → 后置處理器(生成 AOP 代理)。
三級緩存的作用是為 循環(huán)依賴 這種突發(fā)情況 提供一個彈性通道:當 Bean 之間存在循環(huán)依賴時,Spring 可以通過三級緩存提前生成早期引用(可能是代理對象),從而打破循環(huán)依賴的僵局。
換句話說,三級緩存是一種 “在必要時允許突破生命周期規(guī)范的機制”,保證循環(huán)依賴能夠被安全解決,同時不會影響絕大多數 Bean 的正常創(chuàng)建流程。
到此這篇關于對于SpringBoot的三層緩存的思考的文章就介紹到這了,更多相關SpringBoot三層緩存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java實現TXT轉Excel并讀取Excel內容到List集合的完整代碼
在Java開發(fā)中,我們經常會遇到文件格式轉換的需求,比如將txt???文件轉換為??excel???文件,并且可能需要進一步處理??excel文件中的數據,本文將詳細介紹如何使用 Java 代碼完成這個任務,我們會使用???EasyExcel????庫來實現???excel???文件的讀寫操作2025-07-07
Java重入鎖(ReentrantLock)從入門到源碼深度解析
本文給大家介紹Java重入鎖(ReentrantLock)從入門到源碼深度解析,通過本文學習可以全方位地認識ReentrantLock,從基本概念到高級特性,從使用方式到源碼剖析,從底層原理到實際應用,感興趣的朋友跟隨小編一起看看吧2026-02-02

