本篇內容包括:Spring 中的循環依賴問題(包括 Spring 中的循環依賴問題和Spring 中的循環依賴的 5 種場景的介紹)、Spring 三級緩存介紹、4 個 Spring 無法自動解決的循環以來場景以及其對應的手動解決方式。
一、Spring 中的循環依賴問題
1、Spring 中的循環依賴概述
Spring 循環依賴指的是 SpringBean 對象之間的依賴關系形成一個閉環。即在代碼中,把兩個或者多個 Bean 相互之間去持有對方的引用,就會發生循環依賴,循環依賴會導致注入出現死循環,這是 Spring 發生循環依賴的主要原因之一。
Spring 循環依賴主要有三種情況,即:自身依賴自身,兩者互相依賴,多者循環依賴
- 自身依賴自身:自己依賴自己的直接依賴
- 兩者互相依賴:兩個對象之間的直接依賴
- 多者循環依賴:多個對象之間的間接依賴
自身依賴自身,兩者互相依賴 兩者互相依賴的情況比較直觀,很好辨識,但是我們工作中最有可能觸發的還是多者循環依賴,多者循環依賴的情況有時候因為業務代碼調用層級很深,不容易識別出來。但無論循環依賴的數量有多少,循環依賴的本質是一樣的。就是你的完整創建依賴于我,而我的完整創建也依賴于你,但我們互相沒法解耦,最終導致依賴創建失敗。
2、Spring 中的循環依賴的 5 種場景
Spring 中出現循環依賴主要有著 5 種場景: ①、單例的 setter 注入(能解決);②、多例的 setter 注入(不能解決);③、構造器注入(不能解決);④、單例的代理對象 setter 注入(有可能解決);⑤、DependsOn 循環依賴(不能解決)。接下來我們逐一來看。

二、Spring 三級緩存
1、spring 創建 bean 的流程
在開始理解 Spring 三級緩存如何讓解決循環依賴問題前我們先來溫習一下 spring 創建 bean 的流程:
-
Spring 啟動時會根據配置文件或啟動類把所有的 bean 注冊成 bean 定義(就是映射 <bean> 標簽屬性的 Java 類)
-
遍歷 bean 定義中的 beanName,調用 BeanFactory#getBean(beanName) 方法創建、初始化并返回 bean 實例
其中 getBean 方法:
- 先從緩存(一層到三層依次獲取)拿,沒有就去創建;
- 創建 Bean 時,把 beanName 標記為正在創建中,通過其定義里的 class 找到構造器方法反射創建實例,并把其對象工廠放入第三層緩存;
- 對實例初始化,移除正在創建中的標記,把實例放入第一層緩存,移除第二、三層中的緩存,最后返回實例
Ps1:實例初始化過程:獲取此 bean 中有 @Autowired 等注解的成員變量,從所有 bean 定義中找出此類型的 beanName,又通過 BeanFactory#getBean 方法獲取實例,然后反射設值成員變量。
Ps2:上述流程中 斜體 部分為觸發循環依賴時多出主流程的步驟。
位于 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
中的三級緩存源碼:
/** Cache of singleton objects: bean name to bean instance. */private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);/** Cache of singleton factories: bean name to ObjectFactory. */private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);/** Cache of early singleton objects: bean name to bean instance. */private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
2、場景一:單例的 setter 注入
這種注入方式應該是 Spring 中最常見的,Demo 如下:
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}
在上述代碼中,就是一個經典的循環依賴,其中 TestService1 依賴 TestService2,TestService2 依賴 TestService1 構成了一個簡單了兩者互相依賴關系,但是我們在使用類似代碼時,并沒有感知過該類型的循環依賴存在,因為此種類型已經被 Spring 默默解決了。
3、三級緩存
Spring 內部有三級緩存:
- 一級緩存(singletonObjects),用于保存實例化、注入、初始化完成的 Bean 實例
- 二級緩存(earlySingletonObjects),用于保存實例化完成的 Bean 實例
- 三級緩存(singletonFactories),用于保存 Bean 的創建工廠,以便于后面擴展有機會創建代理對象。
以上面 Demo 為例,現在項目啟動,spring 開始創建 bean,比如先創建 TestService1:
- 標記 TestService1 為正在創建中,反射創建其實例,其對象工廠放入第三層緩存
- 初始化 TestService1 實例化時發現需要依賴注入 TestService2,則獲取 TestService2 的實例
- 標記 TestService2 為正在創建中,反射創建其實例,其對象工廠放入第三層緩存
- 初始化 TestService2 實例化時發現需要依賴注入 TestService1,則獲取 TestService1 的實例
- 這時候從緩存中獲取時,TestService1 為正在創建中且第三層緩存有 TestService1 的值了,所以調用緩存的對象工廠的 getObject 方法,把返回的 TestService1 實例放入第二層緩存,刪除第三層緩存
- TestService2 實例初始化完成,放入第一層緩存,移除第二、三層中的緩存
- 回到第 2 步,TestService1 實例初始化完成,放入第一層緩存,移除第二、三層中的緩存
下面是 getBean(beanName) 方法最先調用的從這三層緩存中獲取 bean 實例的邏輯(即上面第5步)
/*** Return the (raw) singleton object registered under the given name.* <p>Checks already instantiated singletons and also allows for an early* reference to a currently created singleton (resolving a circular reference).* @param beanName the name of the bean to look for* @param allowEarlyReference whether early references should be created or not* @return the registered singleton object, or {@code null} if none found*/@Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton locksingletonObject = 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();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}}}return singletonObject;}
以及一直提到的對象工廠,及其 getObject 方法的實現:
/*** Obtain a reference for early access to the specified bean,* typically for the purpose of resolving a circular reference.* @param beanName the name of the bean (for error handling purposes)* @param mbd the merged bean definition for the bean* @param bean the raw bean instance* @return the object to expose as bean reference*/protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (BeanPostProcessor bp : getBeanPostProcessors()) {if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);}}}return exposedObject;}
4、關于二級緩存
細心的朋友可能會發現在這種場景中第二級緩存作用不大。那么問題來了,為什么要用第二級緩存呢?
試想一下,如果出現以下這種情況,我們要如何處理?
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Autowiredprivate TestService3 testService3;public void test1() {}
}
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}
@Service
public class TestService3 {@Autowiredprivate TestService1 testService1;public void test3() {}
}
TestService1 依賴于 TestService2 和 TestService3,而 TestService2 依賴于 TestService1,同時 TestService3 也依賴于 TestService1。按照上圖的流程可以把 TestService1 注入到 TestService2,并且 TestService1 的實例是從第三級緩存中獲取的。
假設不用第二級緩存,TestService1 注入到 TestService3 的流程如圖:

TestService1 注入到 TestService3 又需要從第三級緩存中獲取實例,而第三級緩存里保存的并非真正的實例對象,而是 ObjectFactory對象。說白了,兩次從三級緩存中獲取都是 ObjectFactory 對象,而通過它創建的實例對象每次可能都不一樣的。這樣不是有問題?
為了解決這個問題,Spring 引入的第二級緩存。上面其實 TestService1 對象的實例已經被添加到第二級緩存中了,而在 TestService1 注入到 TestService3 時,只用從第二級緩存中獲取該對象即可。

還有個問題,第三級緩存中為什么要添加 ObjectFactory 對象,直接保存實例對象不行嗎?答:不行,因為假如你想對添加到三級緩存中的實例對象進行增強,直接用實例對象是行不通的。
三、循環依賴的其他 4 種場景
1、多例的 setter 注入
這種注入方法偶然會有,特別是在多線程的場景下,具體代碼如下:
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}
在上述多例的 setter 注入情況下,Spring 程序也是能夠正常啟動啟動的,其實在 AbstractApplicationContext 類的 refresh方法中告訴了我們答案,它會調用 finishBeanFactoryInitialization 方法,該方法的作用是為了 Spring 容器啟動的時候提前初始化一些 Bean。該方法的內部又調用了 preInstantiateSingletons 方法
@Overridepublic void preInstantiateSingletons() throws BeansException {if (logger.isTraceEnabled()) {logger.trace("Pre-instantiating singletons in " + this);}// Iterate over a copy to allow for init methods which in turn register new bean definitions.// While this may not be part of the regular factory bootstrap, it does otherwise work fine.List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);// Trigger initialization of all non-lazy singleton beans...for (String beanName : beanNames) {RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {if (isFactoryBean(beanName)) {Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);if (bean instanceof FactoryBean) {FactoryBean<?> factory = (FactoryBean<?>) bean;boolean isEagerInit;if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,getAccessControlContext());}else {isEagerInit = (factory instanceof SmartFactoryBean &&((SmartFactoryBean<?>) factory).isEagerInit());}if (isEagerInit) {getBean(beanName);}}}else {getBean(beanName);}}}
其中 非抽象、單例 并且非懶加載的類才能被提前初始 Bean。,而多例即 SCOPE_PROTOTYPE 類型的類,非單例,不會被提前初始化 Bean,所以程序能夠正常啟動。如何讓他提前初始化bean呢?
只需要再在 DEMO 中定義一個單例的類,在它里面注入 TestService1
@Service
public class TestService3 {@Autowiredprivate TestService1 testService1;
}
重新啟動程序,執行結果:
Requested bean is currently in creation: Is there an unresolvable circular reference?
果然出現了循環依賴。
Ps:這種循環依賴問題是無法解決的,因為它沒有用緩存,每次都會生成一個新對象。
2、構造器注入
這種注入方式現在其實用的已經非常少了,但是我們還是有必要了解一下,如下代碼:
@Service
public class TestService1 {public TestService1(TestService2 testService2) {}
}
@Service
public class TestService2 {public TestService2(TestService1 testService1) {}
}
運行結果:
Requested bean is currently in creation: Is there an unresolvable circular reference?
出現了循環依賴,為什么呢?
從圖中的流程看出構造器注入沒能添加到三級緩存,也沒有使用緩存,所以也無法解決循環依賴問題。
3、單例的代理對象 setter 注入
這種注入方式其實也比較常用,比如平時使用:@Async 注解的場景,會通過 AOP 自動生成代理對象。
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}
從前面得知程序啟動會報錯,出現了循環依賴,為什么會循環依賴呢?答案就在下面這張圖中:
說白了,Bean 初始化完成之后,后面還有一步去檢查:第二級緩存和原始對象是否相等。由于它對前面流程來說無關緊要,所以前面的流程圖中省略了,但是在這里是關鍵點,我們重點說說:
if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] in its raw version as part of a circular reference, but has eventually been " +"wrapped. This means that said other beans do not use the final version of the " +"bean. This is often the result of over-eager type matching - consider using " +"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");}}}}
正好是走到這段代碼,發現第二級緩存和原始對象不相等,所以拋出了循環依賴的異常。如果這時候把 TestService1 改個名字,改成:TestService6,其他的都不變。
@Service
public class TestService6 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}
再重新啟動一下程序,神奇般的好了。這是為什么?這就要從 Spring Bean 加載順序說起了,默認情況下,Spring 是按照文件完整路徑遞歸查找的,按路徑+文件名排序,排在前面的先加載。所以 TestService1 比T estService2 先加載,而改了文件名稱之后,TestService2 比 TestService6 先加載。
為什么 TestService2 比 TestService6 先加載就沒問題呢?答案在下面這張圖中:
這種情況 testService6 中其實第二級緩存是空的,不需要跟原始對象判斷,所以不會拋出循環依賴。
4、DependsOn 循環依賴
還有一種有些特殊的場景,比如我們需要在實例化 Bean A 之前,先實例化 Bean B,這個時候就可以使用 @DependsOn 注解。
@DependsOn(value = "testService2")
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}
@DependsOn(value = "testService1")
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}
程序啟動之后,執行結果:
Circular depends-on relationship between 'testService2' and 'testService1'
這個例子中本來如果 TestService1 和 TestService2 都沒有加 @DependsOn 注解是沒問題的,反而加了這個注解會出現循環依賴問題。
這又是為什么?答案在 AbstractBeanFactory 類的 doGetBean 方法的這段代碼中:
// Guarantee initialization of beans that the current bean depends on.String[] dependsOn = mbd.getDependsOn();if (dependsOn != null) {for (String dep : dependsOn) {if (isDependent(beanName, dep)) {throw new BeanCreationException(mbd.getResourceDescription(), beanName,"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");}registerDependentBean(dep, beanName);try {getBean(dep);}catch (NoSuchBeanDefinitionException ex) {throw new BeanCreationException(mbd.getResourceDescription(), beanName,"'" + beanName + "' depends on missing bean '" + dep + "'", ex);}}}
它會檢查 dependsOn 的實例有沒有循環依賴,如果有循環依賴則拋異常。
三、出現循環依賴如何解決?
項目中如果出現循環依賴問題,說明是 Spring 默認無法解決的循環依賴,要看項目的打印日志,屬于哪種循環依賴。目前包含下面幾種情況:

解決方式:
問題 | 解決方式 |
---|---|
生成代理對象產生的循環依賴 | ①、 使用 @Lazy 注解,延遲加載 ②、使用 @DependsOn 注解,指定加載先后關系 ③、修改文件名稱,改變循環依賴類的加載順序 |
多例循環依賴 | 可以通過把 Bean 改成單例的解決 |
構造器循環依賴 | 可以通過使用 @Lazy 注解解決 |
使用 @DependsOn 產生的循環依賴 | 要找到@DependsOn注解循環依賴的地方,迫使它不循環依賴就可以解決問題 |