SpringBoot中循環(huán)依賴的常見陷阱與解決方案
引言
在Spring Boot開發(fā)中,你是否遇到過這樣的錯(cuò)誤信息?
The dependencies of some of the beans in the application context form a cycle
這表示你的應(yīng)用出現(xiàn)了循環(huán)依賴。盡管Spring框架通過巧妙的機(jī)制解決了部分循環(huán)依賴問題,但在實(shí)際開發(fā)中(尤其是使用構(gòu)造器注入時(shí)),開發(fā)者仍需警惕此類問題。本文將深入探討循環(huán)依賴的根源,分析Spring的解決策略,并提供多種實(shí)戰(zhàn)解決方案。
一、什么是循環(huán)依賴
循環(huán)依賴指兩個(gè)或多個(gè)Bean相互依賴對(duì)方,形成一個(gè)閉環(huán)。例如:
- ?Bean A? 的創(chuàng)建需要注入 ?Bean B?
- ?Bean B? 的創(chuàng)建又需要注入 ?Bean A?
此時(shí),Spring容器在初始化Bean時(shí)會(huì)陷入“死循環(huán)”。以下是一個(gè)典型示例:
@Service public class ServiceA { private final ServiceB serviceB; public ServiceA(ServiceB serviceB) { // 構(gòu)造器注入ServiceB this.serviceB = serviceB; } } @Service public class ServiceB { private final ServiceA serviceA; public ServiceB(ServiceA serviceA) { // 構(gòu)造器注入ServiceA this.serviceA = serviceA; } }
啟動(dòng)應(yīng)用時(shí),Spring會(huì)拋出異常:
BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Requested bean is currently in creation
二、Spring如何解決循環(huán)依賴
Spring通過三級(jí)緩存機(jī)制解決單例Bean的循環(huán)依賴問題:
- ?一級(jí)緩存?(singletonObjects):存放完全初始化好的Bean。
- ?二級(jí)緩存?(earlySingletonObjects):存放提前曝光的半成品Bean(僅實(shí)例化,未填充屬性)。
- ?三級(jí)緩存?(singletonFactories):存放Bean的工廠對(duì)象,用于生成半成品Bean。
?解決流程?(以A和B相互依賴為例):
- 創(chuàng)建A時(shí),先實(shí)例化A(未填充屬性),并將A的工廠放入三級(jí)緩存。
- 填充A的屬性時(shí)發(fā)現(xiàn)需要B,開始創(chuàng)建B。
- 創(chuàng)建B時(shí),實(shí)例化B后,發(fā)現(xiàn)需要A,此時(shí)從三級(jí)緩存中通過工廠獲取A的半成品對(duì)象。
- B完成初始化,放入一級(jí)緩存。
- A繼續(xù)填充B的實(shí)例,完成初始化,放入一級(jí)緩存。
?關(guān)鍵限制?:該機(jī)制僅支持單例Bean且通過屬性注入的場(chǎng)景。?構(gòu)造器注入會(huì)直接失敗!
三、為何構(gòu)造器注入會(huì)導(dǎo)致循環(huán)依賴失敗
構(gòu)造器注入要求Bean在實(shí)例化階段立即獲得依賴對(duì)象,而三級(jí)緩存機(jī)制需要在屬性注入階段解決依賴。因此,當(dāng)兩個(gè)Bean都使用構(gòu)造器注入時(shí),Spring無法提前曝光半成品Bean,導(dǎo)致循環(huán)依賴無法解決。
四、解決方案:打破循環(huán)依賴的四種方法
1. ?改用Setter/Field注入(謹(jǐn)慎使用)??
將構(gòu)造器注入改為Setter或字段注入,允許Spring延遲注入依賴:
@Service public class ServiceA { private ServiceB serviceB; @Autowired // Setter注入 public void setServiceB(ServiceB serviceB) { this.serviceB = serviceB; } }
?優(yōu)點(diǎn)?:快速解決問題。
?缺點(diǎn)?:破壞了不可變性(字段非final),且可能掩蓋設(shè)計(jì)問題。
2. ?使用@Lazy延遲加載?
在依賴對(duì)象上添加@Lazy,告知Spring延遲初始化Bean:
@Service public class ServiceA { private final ServiceB serviceB; public ServiceA(@Lazy ServiceB serviceB) { this.serviceB = serviceB; // 實(shí)際注入的是代理對(duì)象 } }
?原理?:Spring生成代理對(duì)象,只有在首次調(diào)用時(shí)才會(huì)真正初始化目標(biāo)Bean。
?適用場(chǎng)景?:解決構(gòu)造函數(shù)注入的循環(huán)依賴。
3. ?重新設(shè)計(jì)代碼結(jié)構(gòu)?
通過分層或提取公共邏輯,消除循環(huán)依賴:
?方案一?:引入中間層(如ServiceC),將A和B的共同依賴轉(zhuǎn)移到C。
?方案二?:使用事件驅(qū)動(dòng)(ApplicationEvent),解耦直接依賴。
// 事件驅(qū)動(dòng)示例 @Service public class ServiceA { @Autowired private ApplicationEventPublisher eventPublisher; public void doSomething() { eventPublisher.publishEvent(new EventA()); } } @Service public class ServiceB { @EventListener public void handleEventA(EventA event) { // 處理事件 } }
4. ?使用ObjectProvider(推薦)??
在構(gòu)造器中注入ObjectProvider,按需獲取依賴:
@Service public class ServiceA { private final ServiceB serviceB; public ServiceA(ObjectProvider<ServiceB> serviceBProvider) { this.serviceB = serviceBProvider.getIfUnique(); } }
?優(yōu)點(diǎn)?:保持構(gòu)造器注入的不可變性,顯式控制依賴獲取時(shí)機(jī)。
?注意?:需確保依賴Bean存在且唯一。
五、最佳實(shí)踐與預(yù)防措施
1.?優(yōu)先使用構(gòu)造器注入?:保持Bean的不可變性和明確依賴,但需警惕循環(huán)依賴。
2.?定期檢測(cè)循環(huán)依賴?:
使用IDE插件(如IntelliJ的Circular Dependencies分析)。
通過Maven/Gradle插件(如spring-boot-dependencies-analysis)。
3.?代碼分層規(guī)范?:
嚴(yán)格遵循分層架構(gòu)(Controller → Service → Repository)。
避免同一層內(nèi)的Bean相互依賴。
4.?單元測(cè)試驗(yàn)證?:編寫集成測(cè)試,驗(yàn)證Bean的初始化過程。
@SpringBootTest public class CircularDependencyTest { @Autowired private ApplicationContext context; @Test void contextLoads() { // 若啟動(dòng)無異常,則通過測(cè)試 assertNotNull(context.getBean(ServiceA.class)); } }
六、總結(jié)
循環(huán)依賴是Spring開發(fā)中的常見陷阱,其本質(zhì)是代碼設(shè)計(jì)問題。盡管Spring提供了部分解決方案,但重構(gòu)代碼消除循環(huán)依賴才是根本之道。通過合理使用注入方式、代碼分層和工具檢測(cè),開發(fā)者可以有效避免此類問題,構(gòu)建高可維護(hù)性的應(yīng)用。
?記住?:
- 慎用@Lazy和Setter注入,它們可能掩蓋設(shè)計(jì)缺陷。
- 構(gòu)造器注入 + 合理分層 = 更健壯的系統(tǒng)!
到此這篇關(guān)于SpringBoot中循環(huán)依賴的常見陷阱與解決方案的文章就介紹到這了,更多相關(guān)SpringBoot循環(huán)依賴內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于springboot+vue實(shí)現(xiàn)垃圾分類管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了基于springboot+vue實(shí)現(xiàn)垃圾分類管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07win10 java(jdk安裝)環(huán)境變量配置和相關(guān)問題
這篇文章主要介紹了win10java(jdk安裝)環(huán)境變量配置和相關(guān)問題解決,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-12-12SpringBoot基于Redis實(shí)現(xiàn)短信登錄的操作
驗(yàn)證碼登錄是非常常見的一種登錄方式,能夠簡(jiǎn)化用戶登錄的過程,本文主要介紹了SpringBoot基于Redis實(shí)現(xiàn)短信登錄的操作,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12Spring Boot Admin 環(huán)境搭建與基本使用詳解
這篇文章主要介紹了Spring Boot Admin 環(huán)境搭建與基本使用,本文主要是對(duì)于Spring Boot Admin的基本認(rèn)識(shí)和基本運(yùn)用,通過本篇博客能夠?qū)pring Boot Admin有一個(gè)宏觀認(rèn)知和能夠快速上手,需要的朋友可以參考下2023-08-08java線程并發(fā)blockingqueue類使用示例
BlockingQueue是一種特殊的Queue,若BlockingQueue是空的,從BlockingQueue取東西的操作將會(huì)被阻斷進(jìn)入等待狀態(tài)直到BlocingkQueue進(jìn)了新貨才會(huì)被喚醒,下面是用BlockingQueue來實(shí)現(xiàn)Producer和Consumer的例子2014-01-01