一、背 景
預發環境一個後台服務admin突然啓動失敗,異常如下:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'timeoutNotifyController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'spuCheckDomainServiceImpl': Bean with name 'spuCheckDomainServiceImpl' has been injected into other beans [...] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:598)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:376)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1404)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:592)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:847)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:744)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:391)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:312)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1204)
at com.shizhuang.duapp.commodity.interfaces.admin.CommodityAdminApplication.main(CommodityAdminApplication.java:100)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:51)
at org.springframework.boot.loader.PropertiesLauncher.main(PropertiesLauncher.java:578)
錯誤日誌中明確寫道:“Bean has been injected into other beans ... in its raw version as part of a circular reference, but has eventually been wrapped. ”這不僅僅是一個簡單的循環依賴錯誤。它揭示了一個更深層次的問題:當循環依賴遇上Spring的AOP代理(如@Transactional事務、自定義切面等),Spring在解決依賴的時,不得已將一個“半成品”(原始Bean)注入給了其他30多個Bean。而當這個“半成品”最終被“包裝”(代理)成“成品”時,先前那些持有“半成品”引用的Bean們,使用的卻是一個錯誤的版本。
這就像在組裝一個精密機器時,你把一個未經質檢的零件提前裝了進去,等質檢完成後,機器裏混用着新舊版本的零件,最終的崩潰也就不可避免。
本篇文章將帶你一起:
- 熟悉spring容器的循環依賴以及Spring容器如何解決循環依賴,創建bean相關的流程。
- 深入解讀這條複雜錯誤日誌背後的每一個關鍵線索;
- 提供緊急止血方案;
- 分享如何從架構設計上避免此類問題的實踐心得。
二、相關知識點簡介
2.1 循環依賴
什麼是Bean循環依賴?
循環依賴:説白是一個或多個對象實例之間存在直接或間接的依賴關係,這種依賴關係構成了構成一個環形調用,主要有如下幾種情況。
第一種情況:自己依賴自己的直接依賴
第二種情況:兩個對象之間的直接依賴
前面兩種情況的直接循環依賴比較直觀,非常好識別,但是第三種間接循環依賴的情況有時候因為業務代碼調用層級很深,不容易識別出來。
循環依賴場景
構造器注入循環依賴:
@Service
public class A {public A(B b) {}}
@Service
public class B {public B(A a) {}}
結果:項目啓動失敗拋出異常BeanCurrentlyInCreationException
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
構造器注入構成的循環依賴,此種循環依賴方式無論是Singleton模式還是prototype模式都是無法解決的,只能拋出BeanCurrentlyInCreationException異常表示循環依賴。原因是Spring解決循環依賴依靠的是Bean的“中間態”這個概念,而中間態指的是已經實例化,但還沒初始化的狀態。而完成實例化需要調用構造器,所以構造器的循環依賴無法解決。
Singleton模式field屬性注入(setter方法注入)循環依賴:
這種方式是我們最為常用的依賴注入方式:
@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
結果:項目啓動成功,正常運行
prototype field屬性注入循環依賴:
prototype在平時使用情況較少,但是也並不是不會使用到,因此此種方式也需要引起重視。
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class A {
@Autowired
private B b;
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class B {
@Autowired
private A a;
}
結果:需要注意的是本例中啓動時是不會報錯的(因為非單例Bean默認不會初始化,而是使用時才會初始化),所以很簡單咱們只需要手動getBean()或者在一個單例Bean內@Autowired一下它即可。
// 在單例Bean內注入
@Autowired
private A a;
這樣子啓動就報錯:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'mytest.TestSpringBean': Unsatisfied dependency expressed through field 'a'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'a': Unsatisfied dependency expressed through field 'b'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'b': Unsatisfied dependency expressed through field 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:596)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374)
如何解決?可能有的小夥伴看到網上有説使用@Lazy註解解決:
@Lazy
@Autowired
private A a;
此處負責任的告訴你這樣是解決不了問題的(可能會掩蓋問題),@Lazy只是延遲初始化而已,當你真正使用到它(初始化)的時候,依舊會報如上異常。
對於Spring循環依賴的情況總結如下:
- 不能解決的情況:構造器注入循環依賴,prototype field屬性注入循環依賴
- 能解決的情況:field屬性注入(setter方法注入)循環依賴
Spring如何解決循環依賴
Spring 是通過三級緩存和提前曝光的機制來解決循環依賴的問題。
三級緩存
三級緩存其實就是用三個 Map 來存儲不同階段 Bean 對象。
一級緩存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
二級緩存
private final Map<String, ObjectearlySingletonObjects = new HashMap<>(16);
//三級緩存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16)
- singletonObjects:用於存放完全初始化好的 bean,從該緩存中取出的 bean 可以直接使用。
- earlySingletonObjects:提前曝光的單例對象的cache,存放原始的 bean 對象(尚未填充屬性),用於解決循環依賴。
- singletonFactories:單例對象工廠的cache,存放 bean 工廠對象,用於解決循環依賴。
三級緩存解決循環依賴過程
假設現在我們有ServiceA和ServiceB兩個類,這兩個類相互依賴,代碼如下:
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA ;
}
下面的時序圖説明了spring用三級緩存解決循環依賴的主要流程:
為什麼需要三級緩存?
這是一個理解Spring容器如何解決循環依賴的核心概念。三級緩存是Spring為了解決循環依賴的同時,又能保證AOP代理的正確性而設計的精妙機制。
為了理解為什麼需要三級緩存,我們一步步來看。
如果沒有緩存(Level 0)
假設有兩個Bean:ServiceA 和 ServiceB,它們相互依賴。
Java
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Component
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
創建過程(無緩存) :
- 開始創建 ServiceA -> 發現 ServiceA 需要 ServiceB -> 開始創建 ServiceB
- 開始創建 ServiceB -> 發現 ServiceB 需要 ServiceA -> 開始創建 ServiceA
- 開始創建 ServiceA -> 發現 ServiceA 需要 ServiceB -> ... 無限循環,StackOverflowError
結論:無法解決循環依賴,直接死循環。
如果只有一級緩存(Singleton Objects)
一級緩存存放的是已經完全創建好、初始化完畢的Bean。
問題:在Bean的創建過程中(比如在填充屬性 populateBean 時),ServiceA還沒創建完,它本身不應該被放入"已完成"的一級緩存。但如果ServiceB需要ServiceA,而一級緩存裏又沒有ServiceA的半成品,ServiceB就無法完成創建。這就回到了上面的死循環問題。
結論:一級緩存無法解決循環依賴。
如果使用二級緩存
二級緩存的核心思路是:將尚未完全初始化好的“早期引用”暴露出來。
現在我們有:
- 一級緩存(成品庫) :存放完全準備好的Bean。
- 二級緩存(半成品庫) :存放剛剛實例化(調用了構造方法),但還未填充屬性和初始化的Bean的早期引用。
創建過程(二級緩存):
開始創建ServiceA:
- 實例化ServiceA(調用ServiceA的構造方法),得到一個ServiceA的原始對象。
- 將ServiceA的原始對象放入二級緩存(半成品庫)。
- 開始為ServiceA填充屬性 -> 發現需要ServiceB。
開始創建ServiceB:
- 實例化ServiceB(調用B的構造方法),得到一個ServiceB的原始對象。
- 將ServiceB的原始對象放入二級緩存。
- 開始為ServiceB填充屬性 -> 發現需要ServiceA。
ServiceB從二級緩存中獲取A:
- ServiceB成功從二級緩存中拿到了ServiceA的早期引用(原始對象)。
- ServiceB順利完成了屬性填充、初始化等後續步驟,成為一個完整的Bean。
- 將完整的ServiceB放入一級緩存(成品庫),並從二級緩存移除ServiceB。
ServiceA繼續創建:
- ServiceA拿到了創建好的ServiceB,完成了自己的屬性填充和初始化。
- 將完整的ServiceA放入一級緩存(成品庫),並從二級緩存移除ServiceA。
問題來了:如果ServiceA需要被AOP代理怎麼辦?
如果A類上加了 @Transactional 等需要創建代理的註解,那麼最終需要暴露給其他Bean的應該是ServiceA的代理對象,而不是ServiceA的原始對象。
在二級緩存方案中,ServiceB拿到的是A的原始對象。但最終ServiceA完成後,放入一級緩存的是ServiceA的代理對象。這就導致了:
- ServiceB裏面持有的ServiceA是原始對象。
- 而其他地方注入的ServiceA是代理對象。
- 這就造成了不一致!如果通過ServiceB的ServiceA去調用事務方法,事務會失效,因為那是一個沒有被代理的原始對象。
結論:二級緩存可以解決循環依賴問題,但無法正確處理需要AOP代理的Bean。
三級緩存的登場(Spring的終極方案)
為了解決代理問題,Spring引入了第三級緩存。它的核心不是一個直接存放對象(Object)的緩存,而是一個存放 ObjectFactory(對象工廠) 的緩存。
三級緩存的結構是:Map<String, ObjectFactory<?>> singletonFactories
創建過程(三級緩存,以ServiceA需要代理為例):
- 開始創建ServiceA:
<!---->
- 實例化ServiceA,得到ServiceA的原始對象。
- 向三級緩存添加一個ObjectFactory。這個工廠的getObject()方法有能力判斷ServiceA是否需要代理,並返回相應的對象(原始對象或代理對象) 。
- 開始為ServiceA填充屬性 -> 發現需要ServiceB。
- - 開始創建B:
- 實例化ServiceB。
- 同樣向三級緩存添加一個ServiceB的ObjectFactory。
- 開始為ServiceB填充屬性 -> 發現需要ServiceA。
- - ServiceB從緩存中獲取ServiceA:
- ServiceB發現一級緩存沒有ServiceA,二級緩存也沒有ServiceA。
- ServiceB發現三級緩存有A的ObjectFactory。
- B調用這個工廠的getObject()方法。此時,Spring會執行一個關鍵邏輯:
- 如果ServiceA需要被代理,工廠會提前生成ServiceA的代理對象並返回。
- 如果ServiceA不需要代理,工廠則返回A的原始對象。
- 將這個早期引用(可能是原始對象,也可能是代理對象) 放入二級緩存,同時從三級緩存移除A的工廠。
- ServiceB拿到了ServiceA的正確版本的早期引用。
後續步驟:
- ServiceB完成創建,放入一級緩存。
- ServiceA繼續用ServiceB完成創建。在ServiceA初始化的最後,Spring會再次檢查:如果ServiceA已經被提前代理了(即在第3步中),那麼就直接返回這個代理對象;如果沒有,則可能在此處創建代理(對於不需要解決循環依賴的Bean)。
- 最終,將完整的ServiceA(代理對象)放入一級緩存,並清理二級緩存。
總結:為什麼需要三級緩存?
需要三級緩存,是因為Spring要解決一個複雜問題:在存在循環依賴的情況下,如何確保所有Bean都能拿到最終形態(可能被AOP代理)的依賴對象,而不是原始的、未代理的對象。 三級緩存通過一個ObjectFactory將代理的時機提前,完美地解決了這個問題。二級緩存主要是為了性能優化而存在的。
spring三級緩存為什麼不能解決
@Async註解的循環依賴問題
這觸及了 Spring 代理機制的一個深層次區別。@Async註解的循環依賴問題確實比@Transactional 更復雜,三級緩存無法完全解決。讓我們深入分析原因。
2.2 Spring創建Bean主要流程
為了容易理解 Spring 解決循環依賴過程,我們先簡單温習下 Spring 容器創建 Bean 的主要流程。
從代碼看Spring對於Bean的生成過程,步驟還是很多的,我把一些擴展業務代碼省略掉:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
// Bean初始化第一步:默認調用無參構造實例化Bean
// 如果是隻有帶參數的構造方法,構造方法裏的參數依賴注入,就是發生在這一步
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
//判斷Bean是否需要提前暴露對象用來解決循環依賴,需要則啓動spring三級緩存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// Initialize the bean instance.
Object exposedObject = bean;
try {
// bean創建第二步:填充屬性(DI依賴注入發生在此步驟)
populateBean(beanName, mbd, instanceWrapper);
// bean創建第三步:調用初始化方法,完成bean的初始化操作(AOP的第三個入口)
// AOP是通過自動代理創建器AbstractAutoProxyCreator的postProcessAfterInitialization()
//方法的執行進行代理對象的創建的,AbstractAutoProxyCreator是BeanPostProcessor接口的實現
exposedObject = initializeBean(beanName, exposedObject, mbd);
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 " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
} catch (Throwable ex) {
// ...
}
// ...
return exposedObject;
}
從上述代碼看出,整體脈絡可以歸納成 3 個核心步驟:
- 實例化Bean:主要是通過反射調用默認構造函數創建 Bean 實例,此時Bean的屬性都還是默認值null。被註解@Bean標記的方法就是此階段被調用的。
- 填充Bean屬性:這一步主要是對Bean的依賴屬性進行填充,對@Value、@Autowired、@Resource註解標註的屬性注入對象引用。
- 調用Bean初始化方法:調用配置指定中的init方法,如 xml文件指定Bean的init-method方法或註解 @Bean(initMethod = "initMethod")指定的方法。
三、案例分析
3.1 代碼分析
以下是我簡化後的類之間大體的依賴關係,工程內實際的依賴情況會比這個簡化版本複雜一些。
@RestController
public class OldCenterSpuController {
@Resource
private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}
@RestController
public class TimeoutNotifyController {
@Resource
private SpuCheckDomainServiceImpl spuCheckDomainServiceImpl;
}
@Component
public class NewSpuApplyCheckServiceImpl {
@Resource
private SpuCheckDomainServiceImpl spuCheckDomainServiceImpl;
}
@Component
@Slf4j
@Validated
public class SpuCheckDomainServiceImpl {
@Resource
private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}
從代碼看,主要是SpuCheckDomainServiceImpl和NewSpuApplyCheckServiceImpl 構成了一個依賴環。而我們從正常啓動的bean加載順序發現首先是從OldCenterSpuController開始加載的,具體情況如下所示:
OldCenterSpuController
↓ (依賴)
NewSpuApplyCheckServiceImpl
↓ (依賴)
SpuCheckDomainServiceImpl
↓ (依賴)
NewSpuApplyCheckServiceImpl
異常啓動的情況bean加載是從TimeoutNotifyController開始加載的,具體情況如下所示:
TimeoutNotifyController
↓ (依賴)
SpuCheckDomainServiceImpl
↓ (依賴)
NewSpuApplyCheckServiceImpl
↓ (依賴)
SpuCheckDomainServiceImpl
同一個依賴環,為什麼從OldCenterSpuController 開始加載就可以正常啓動,而從TimeoutNotifyController 啓動就會啓動異常呢?下面我們會從現場debug的角度來分析解釋這個問題。
3.2 問題分析
在相關知識點簡介裏面知悉到spring用三級緩存解決了循環依賴問題。為什麼後台服務admin啓動還會報循環依賴的問題呢?
要得到問題的答案,還是需要回到源碼本身,前面我們分析了spring的創建Bean的主要流程,這裏為了更好的分析問題,補充下通過容器獲取Bean的。
在通過spring容器獲取bean時,底層統一會調用doGetBean方法,大體如下:
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
final String beanName = transformedBeanName(name);
Object bean;
// 從三級緩存獲取bean
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}else {
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
//如果是單例Bean,從三級緩存沒有獲取到bean,則執行創建bean邏輯
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
}
從doGetBean方法邏輯看,在spring從一二三級緩存獲取bean返回空時,會調用createBean方法去場景bean,createBean方法底層主要是調用前面我們提到的創建Bean流程的doCreateBean方法。
注意:doGetBean方法裏面getSingleton方法的邏輯是先從一級緩存拿,拿到為空並且bean在創建中則又從二級緩存拿,二級緩存拿到為空 並且當前容器允許有循環依賴則從三級緩存拿。並且將對象工廠移到二級緩存,刪除三級緩存
doCreateBean方法如下:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
// Bean初始化第一步:默認調用無參構造實例化Bean
// 如果是隻有帶參數的構造方法,構造方法裏的參數依賴注入,就是發生在這一步
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
//判斷Bean是否需要提前暴露對象用來解決循環依賴,需要則啓動spring三級緩存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// Initialize the bean instance.
Object exposedObject = bean;
try {
// bean創建第二步:填充屬性(DI依賴注入發生在此步驟)
populateBean(beanName, mbd, instanceWrapper);
// bean創建第三步:調用初始化方法,完成bean的初始化操作(AOP的第三個入口)
// AOP是通過自動代理創建器AbstractAutoProxyCreator的postProcessAfterInitialization()
//方法的執行進行代理對象的創建的,AbstractAutoProxyCreator是BeanPostProcessor接口的實現
exposedObject = initializeBean(beanName, exposedObject, mbd);
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 " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
} catch (Throwable ex) {
// ...
}
// ...
return exposedObject;
}
將doGetBean和doCreateBean的邏輯轉換成流程圖如下:
從流程圖可以看出,後台服務admin啓動失敗拋出UnsatisfiedDependencyException異常的必要條件是存在循環依賴,因為不存在循環依賴的情況bean只會存在單次加載,單次加載的情況bean只會被放進spring的第三級緩存。
而觸發UnsatisfiedDependencyException異常的先決條件是需要spring的第一二級緩存有當前的bean。所以可以知道當前bean肯定存在循環依賴。在存在循環依賴的情況下,當前bean被第一次獲取(即調用doGetBean方法)會緩存進spring的第三級緩存,然後會注入當前bean的依賴(即調用populateBean方法),在當前bean所在依賴環內其他bean都不在一二級緩存的情況下,會觸發當前bean的第二次獲取(即調用doGetBean方法),由於第一次獲取已經將Bean放進了第三級緩存,spring會將Bean從第三級緩存移到二級緩存並刪除第三級緩存。
最終會回到第一次獲取的流程,調用初始化方法做初始化。最終在初始化有對當前bean做代理增強的並且提前暴露到二級緩存的對象有被其他依賴引用到,而且allowRawInjectionDespiteWrapping=false的情況下,會導致拋出UnsatisfiedDependencyException,進而導致啓動異常。
注意:在注入當前bean的依賴時,這裏spring將Bean從第三級緩存移到二級緩存並刪除第三級緩存後,當前bean的依賴的其他bean會從二級緩存拿到當前bean做依賴。這也是後續拋異常的先決條件
結合admin有時候啓動正常,有時候啓動異常的情況,這裏猜測啓動正常和啓動異常時bean加載順序不一致,進而導致啓動正常時當前Bean只會被獲取一次,啓動異常時當前bean會被獲取兩次。為了驗證猜想,我們分別針對啓動異常和啓動正常的bean獲取做了debug。
debug分析
首先我們從啓動異常提取到以下關鍵信息,從這些信息可以知道是spuCheckDomainServiceImpl的加載觸發的啓動異常。所以我們這裏以spuCheckDomainServiceImpl作為前面流程分析的當前bean。
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'timeoutNotifyController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'spuCheckDomainServiceImpl': Bean with name 'spuCheckDomainServiceImpl' has been injected into other beans [...] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
然後提前我們在doCreateBean方法設置好spuCheckDomainServiceImpl加載時的條件斷點。我們先debug啓動異常的情況。最終斷點信息如下:
從紅框1裏面的兩個引用看,很明顯調initializeBean方法時spring有對spuCheckDomainServiceImpl做代理增強。導致initializeBean後返回的引用和提前暴露到二級緩存的引用是不一致的。這裏spuCheckDomainServiceImpl有二級緩存是跟我們前面分析的吻合,是因為spuCheckDomainServiceImpl被獲取了兩次,即調了兩次doGetBean。
從紅框2裏面的actualDependentBeans的set集合知道提前暴露到二級緩存的引用有被其他33個bean引用到,也是跟異常提示的bean列表保持一致的。
這裏spuCheckDomainServiceImpl的加載為什麼會調用兩次doGetBean方法呢?
從調用棧分析到該加載鏈如下:
TimeoutNotifyController ->spuCheckDomainServiceImpl-> newSpuApplyCheckServiceImpl-> ... ->spuCheckDomainServiceImpl
TimeoutNotifyController注入依賴時第一次調用doGetBean獲取spuCheckDomainServiceImpl時,從一二三級緩存獲取不到,會調用doCreateBean方法創建spuCheckDomainServiceImpl。
首先會將spuDomainServiceImpl放進spring的第三級緩存,然後開始調populateBean方法注入依賴,由於在循環中間的newSpuApplyCheckServiceImpl是第一次獲取,一二三級緩存都獲取不到,會調用doCreateBean去創建對應的bean,然後會第二次調用doGetBean獲取spuCheckDomainServiceImpl,這時spuCheckDomainServiceImpl在第一次獲取已經將bean加載到第三級緩存,所以這次spring會將bean從第三級緩存直接移到第二級緩存,並將第三級緩存裏面的spuCheckDomainServiceImpl對應的bean刪除,並直接返回二級緩存裏面的bean,不會再調doCreateBean去創建spuCheckDomainServiceImpl。最終完成了循環中間的bean的初始化後(這裏循環中間的bean初始化時依賴到的bean如果有引用到spuCheckDomainServiceImpl會調用doGetBean方法從二級緩存拿到spuCheckDomainServiceImpl提前暴露的引用),會回到第一次調用doGetBean獲取spuCheckDomainServiceImpl時調用的doCreateBean方法的流程。繼續調initializeBean方法完成初始化,然後將初始化完成的bean返回。最終拿初始化返回的bean引用跟二級緩存拿到的bean引用做對比,發現不一致,導致拋出UnsatisfiedDependencyException異常。
那麼這裏為什麼spuCheckDomainServiceImpl調用initializeBean方法完成初始化後與提前暴露到二級緩存的bean會不一致呢?
看spuCheckDomainServiceImpl的代碼如下:
@Component
@Slf4j
@Validated
public class SpuCheckDomainServiceImpl {
@Resource
private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}
發現SpuCheckDomainServiceImpl類有使用到 @Validated註解。查閲資料發現 @Validated的實現是通過在initializeBean方法裏面執行一個org.springframework.validation.beanvalidation.MethodValidationPostProcessor後置處理器實現的,MethodValidationPostProcessor會對SpuCheckDomainServiceImpl做一層代理。導致initializeBean方法返回的spuCheckDomainServiceImpl是一個新的代理對象,從而最終導致跟二級緩存的不一致。
debug視圖如下:
那為什麼有時候能啓動成功呢?什麼情況下能啓動成功?
我們繼續debug啓動成功的情況。最終觀察到spuCheckDomainServiceImpl只會調用一次doGetBean,而且從一二級緩存拿到的spuCheckDomainServiceImpl提前暴露的引用為null,如下圖:
這裏為什麼spuCheckDomainServiceImpl只會調用一次doGetBean呢?
首先我們根據調用棧整理到當前加載的引用棧:
oldCenterSpuController-> newSpuApplyCheckServiceImpl-> ... ->spuCheckDomainServiceImpl -> newSpuApplyCheckServiceImpl
根據前面啓動失敗的信息我們可以知道,spuCheckDomainServiceImpl處理依賴的環是:
spuCheckDomainServiceImpl ->newSpuApplyCommandServiceImpl-> ... ->spuCheckDomainServiceImpl
失敗的情況我們發現是從spuCheckDomainServiceImpl開始創建的,現在啓動正常的情況是從newSpuApplyCheckServiceImpl開始創建的。
創建 newSpuApplyCheckServiceImpl時,發現它依賴環中間這些bean會依次調用doCreateBean方法去創建對應的bean。
調用到spuCheckDomainServiceImpl時,由於是第一次獲取bean,也會調用doCreateBean方法創建bean,然後回到創建spuCheckDomainServiceImpl的doCreateBean流程,這裏由於沒有將spuCheckDomainServiceImpl的三級緩存移到二級緩存,所以不會導致拋出UnsatisfiedDependencyException異常,最終回到newSpuApplyCheckServiceImpl的doCreateBean流程,由於newSpuApplyCheckServiceImpl在調用initializeBean方法沒有做代理增強,所以也不會導致拋出UnsatisfiedDependencyException異常。因此最後可以正常啓動。
這裏我們會有疑問?類的創建順序由什麼決定的呢?
通常不同環境下,代碼打包後的jar/war結構、@ComponentScan的basePackages配置細微差別,都可能導致Spring掃描和註冊Bean定義的順序不同。Java ClassLoader加載類的順序本身也有一定不確定性。如果Bean定義是通過不同的配置類引入的,配置類的加載順序會影響其中所定義Bean的註冊順序。
那是不是所有的類增強在有循環依賴時都會觸發UnsatisfiedDependencyException異常呢?
並不是,比如@Transactional就不會導致觸發UnsatisfiedDependencyException異常。讓我們深入分析原因。
核心區別在於代理創建時機不同。
@Transactional的代理時機如下:
// Spring 為 @Transactional 創建代理的流程1. 實例化原始 Bean
2. 放入三級緩存(ObjectFactory)
3. 當發生循環依賴時,調用 ObjectFactory.getObject()
4. 此時判斷是否需要事務代理,如果需要則提前創建代理
5. 將代理對象放入二級緩存,供其他 Bean 使用
@Validated的代理時機:
// @Validated 的代理創建在生命週期更晚的階段1. 實例化原始 Bean
2. 放入三級緩存(ObjectFactory)
3. 當發生循環依賴時,調用 ObjectFactory.getObject()
4. ❌ 問題:此時 @Validated 的代理還未創建!
5. 其他 Bean 拿到的是原始對象,而不是異步代理對象
問題根源:@Transactional的代理增強是在三層緩存生成時觸發的, @Validated的增強是在初始化bean後通過後置處理器做的代理增強。
3.3 解決方案
短期方案
- 移除SpuCheckDomainServiceImpl類上的Validated註解
- @lazy 解耦
-
- 原理是發現有@lazy 註解的依賴為其生成代理類,依賴代理類,只有在真正需要用到對象時,再通過getBean的邏輯去獲取對象,從而實現瞭解耦。
長期方案
嚴格執行DDD代碼規範
這裏是違反DDD分層規範導致的循環依賴。
梳理解決歷史依賴環
通過梳理修改代碼解決歷史存在的依賴環。我們內部實現了一個能檢測依賴環的工具,這裏簡單介紹一下實現思路,詳情如下。
日常循環依賴環:實戰檢測工具類解析
在實際項目中,即使遵循了DDD分層規範和注入最佳實踐,仍有可能因業務複雜或團隊協作不充分而引入循環依賴。為了在開發階段儘早發現這類問題,我們可以藉助自定義的循環依賴檢測工具類,在Spring容器啓動後自動分析並報告依賴環。
功能概述:
- 條件啓用:通過配置circular.dependecy.analysis.enabled=true開啓檢測;
- 依賴圖構建:掃描所有單例Bean,分析其構造函數、字段、方法注入及depends-on聲明的依賴;
- 循環檢測算法:使用DFS遍歷依賴圖,識別所有循環依賴路徑;
- 通知上報:檢測結果通過飛書機器人發送至指定接收人(targetId)。
簡潔代碼結構如下:
@Component
@ConditionalOnProperty(value = "circular.dependency.analysis.enabled", havingValue = "true")
public class TimingCircularDependencyHandler extends AbstractNotifyHandler<NotifyData>
implements ApplicationContextAware, BeanFactoryAware {
@Override
public Boolean handler(NotifyData data) {
dependencyGraph = new HashMap<>();
handleContextRefresh(); // 觸發依賴圖構建與檢測
return Boolean.TRUE;
}
private void buildDependencyGraph() {
// 遍歷所有Bean,解析其依賴關係
// 支持:構造器、字段、方法、depends-on
}
private void detectCircularDependencies() {
// 使用DFS檢測環,記錄所有循環路徑
// 輸出示例:循環依賴1: A -> B -> C -> A
}
}
四、總結
循環依賴暴露了代碼結構的設計缺陷。理論上應通過分層和抽象來避免,但在複雜的業務交互中仍難以杜絕。雖然Spring利用三級緩存等機制默默解決了這一問題,使程序得以運行,但這絕不應是懈怠設計的藉口。我們更應恪守設計原則,從源頭規避循環依賴,構建清晰、健康的架構。
往期回顧
1. Apex AI輔助編碼助手的設計和實踐|得物技術
2. 從 JSON 字符串到 Java 對象:Fastjson 1.2.83 全程解析|得物技術
3. 用好 TTL Agent 不踩雷:避開內存泄露與CPU 100%兩大核心坑|得物技術
4. 線程池ThreadPoolExecutor源碼深度解析|得物技術
5. 基於瀏覽器擴展 API Mock 工具開發探索|得物技術
文 /魯班
關注得物技術,每週更新技術乾貨
要是覺得文章對你有幫助的話,歡迎評論轉發點贊~
未經得物技術許可嚴禁轉載,否則依法追究法律責任。