一句話先說結論:Bean 循環依賴就是 A 依賴 B、B 又依賴 A,Spring 創建時先給 bean 打上「正在創建」標記,回頭再要同一個 bean 就撞上
BeanCurrentlyInCreationException。Spring Boot 2.6 起把spring.main.allow-circular-references預設值從true改成了false,所以老項目一升級就啟動失敗。最該做的是重構拆掉這個環;圖快可以用@Lazy或換字段/setter 注入,臨時兜底才去開那個開關。
背景
Spring 的 ApplicationContext 在啟動階段會按依賴關係把一個個 bean 實例化並組裝起來。正常情況下,這張依賴圖是一棵有向無環的樹。循環依賴讓圖裡出現了環,等於「先有雞還是先有蛋」——容器沒法把兩邊的物件都完整造出來。
最常見的現場是兩個 @Service 互相注入:OrderService 要調用 ProductService 查商品,ProductService 又要調 OrderService 查訂單。業務上看著合理,結構上卻已經耦合過緊。
現象
升級到 2.6+,或者直接改成構造器注入,應用啟動直接失敗。控制台常見兩種報法:
舊樣式是底層例外:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'orderService': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:347)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:235)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:423)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:523)
2.6+ 的 FailureAnalysis 報得更直觀,會直接把環畫出來:
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| orderService defined in file [.../OrderService.class]
↑ ↓
| productService defined in file [.../ProductService.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default.
Update your application to remove the dependency cycle between beans.
As a last resort, it may be possible to break the cycle automatically by setting
spring.main.allow-circular-references to true.
能觸發這套報錯的復現程式碼長這樣:
@Service
public class OrderService {
private final ProductService productService;
public OrderService(ProductService productService) {
this.productService = productService;
}
}
@Service
public class ProductService {
private final OrderService orderService;
public ProductService(OrderService orderService) {
this.orderService = orderService;
}
}
根因分析
Spring 創建單例 bean 時,第一步先把它放進 singletonsCurrentlyInCreation,標記「正在創建」。OrderService 的構造器需要 ProductService,容器就轉去創建 ProductService;而 ProductService 的構造器又要 OrderService,容器一看「這個 bean 我正在建呢」,於是拋出 BeanCurrentlyInCreationException。
構造器注入之所以無解,是因為必須先拿到所有依賴才能調用構造器完成實例化,中間不存在「半成品」可以提前暴露。而字段/setter 注入則不同——Spring 框架本身有三級緩存(singletonFactories、earlySingletonObjects 這些),能在屬性注入階段把還沒完全初始化的 bean 提前暴露出來,所以 2.5 之前預設就能「繞過去」。
真正的分水嶺是 Spring Boot 2.6.0:它把 spring.main.allow-circular-references 的預設值從 true 改成了 false。也就是說,哪怕你用的是字段/setter 注入,2.6 之後也會在啟動時被主動攔下來。這不是 bug,而是刻意為之——循環依賴往往是設計缺陷,fail fast 逼你去修,而不是讓半初始化的 bean 在運行時埋雷。
排查時最有用的就是上面那段 cycle 圖,它會列出環上每一個 bean。如果還看不清,就把 bean 的日誌級別調成 DEBUG:
logging.level.org.springframework.beans=DEBUG
解決方案
首選:重構,拆掉環
從根本上讓依賴單向化。最常見的做法是把雙方共用的邏輯抽到第三個 bean 裡:
@Service
public class PricingService {
public Money calc(Order order) { /* ... */ }
}
@Service
public class OrderService {
private final PricingService pricing;
public OrderService(PricingService pricing) { this.pricing = pricing; }
}
@Service
public class ProductService {
private final PricingService pricing;
public ProductService(PricingService pricing) { this.pricing = pricing; }
}
環消失了,兩個 service 都只依賴 PricingService。這是最乾淨、最可持續的方案,也是官方推薦的方向。
@Lazy:打斷即時初始化
在環的任意一邊注入點加 @Lazy,Spring 會先注入一個代理,真正用到時才去初始化被依賴的 bean,從而打斷啟動時的死鎖:
@Service
public class OrderService {
private final ProductService productService;
public OrderService(@Lazy ProductService productService) {
this.productService = productService;
}
}
構造器、字段、setter 三種注入都適用。代價是這只是繞過,不是消除,程式碼裡依然存在強耦合,而且延遲初始化可能把錯誤推遲到運行時才暴露。
字段 / setter 注入 + 打開開關(臨時兜底)
如果 2.6 之前用字段注入一直沒問題、升級後才踩雷,短期內可以在設定裡放開:
spring:
main:
allow-circular-references: true
開關打開後,字段或 setter 注入能借助三級緩存正常構建,但構造器注入依舊不行。這個開關在官方文檔裡被明確標為 last resort,屬於遷移過渡的臨時手段,不建議長期依賴——你等於保留下了一個設計缺陷,而且它可能在未來的版本里被徹底移除。
小結
- 這個報錯的本質是依賴圖成環,不是 Spring 抽風。
- 升級到 2.6+ 最容易撞上,因為預設禁止循環依賴了。
- 優先重構拆環;圖快用
@Lazy;別圖省事長期開著allow-circular-references。 - 排查先從 cycle 圖入手,必要時打開
logging.level.org.springframework.beans=DEBUG。
References(參考來源)
- Resolve Circular Dependency in Spring Boot — LightTraceblog
- Fix: Spring Boot "The dependencies of some of the beans in the application context form a cycle" — FixDevsblog
- Fix: Spring BeanCreationException — FixDevsblog
- SpringBoot 2.6.x 預設禁用循環依賴後的應對策略 — springcloud.ioblog
- spring.main.allow-circular-references — Spring Boot 設定說明blog
