Spring Boot 2.6.0+ 循環依賴問題及解決方案
目錄
- 背景
- 解決方案
- 1. 配置文件開啟循環依賴(侵入性最低,臨時方案)
- 2. @Lazy 延遲注入(侵入性低,推薦優先嘗試)
- 3. 手動從容器獲取(ApplicationContextAware,侵入性中等)
- 4. 接口隔離 / 中間層解耦(侵入性高,推薦長期方案)
- 5. 事件驅動(ApplicationEvent,侵入性中等,適合通知場景)
- 總結
- 版本差異補充
背景
從 Spring Boot 2.6.0 開始,Spring 團隊默認禁止了循環依賴(circular references),這是一個重要的設計變更。主要原因包括:
- 設計原則:循環依賴通常暗示著不良的代碼設計,違反了單一職責原則
- 初始化問題:循環依賴可能導致難以預測的bean初始化順序和狀態
- 調試困難:循環依賴使得問題排查變得復雜
- 性能考慮:三級緩存機制增加了額外的內存開銷
配置變更:
spring:main:allow-circular-references: false # 2.6+ 默認值
常見錯誤信息:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| dictionaryServiceImpl defined in file [...DictionaryServiceImpl.class]
↑ ↓
| dictionaryDataServiceImpl defined in file [...DictionaryDataServiceImpl.class]
└─────┘
解決方案
1. 配置文件開啟循環依賴(侵入性最低,臨時方案)
適用場景:快速解決遺留項目的啟動問題,臨時過渡方案
# application.yml
spring:main:allow-circular-references: true
優點:
- 零代碼修改
- 立即生效
- 保持原有業務邏輯不變
缺點:
- 治標不治本
- 可能隱藏潛在的設計問題
- 不符合Spring新版本的設計理念
2. @Lazy 延遲注入(侵入性低,推薦優先嘗試)
適用場景:簡單的雙向依賴,不需要在初始化時立即使用依賴對象
實現方式:
@Service
public class DictionaryDataServiceImpl implements DictionaryDataService {@Lazy // 延遲加載,打破循環依賴@Resourceprivate DictionaryService dictionaryService;public void someMethod() {// 只有在真正調用時才會初始化 dictionaryServiceDictionary dict = dictionaryService.getById(1);}
}@Service
public class DictionaryServiceImpl implements DictionaryService {@Resourceprivate DictionaryDataService dictionaryDataService; // 保持正常注入// 業務邏輯...
}
工作原理:
@Lazy
注入的是一個代理對象,而非真實的bean- 只有在第一次調用方法時,才會觸發真實bean的創建
- 從而避開了啟動時的循環依賴檢查
優點:
- 代碼侵入性小
- 保持了依賴注入的便利性
- 符合Spring的設計理念
缺點:
- 首次調用時性能略有損失
- 需要明確哪個依賴使用@Lazy
3. 手動從容器獲取(ApplicationContextAware,侵入性中等)
適用場景:需要更靈活的依賴獲取方式,或者依賴關系比較復雜
實現方式一:ApplicationContextAware接口
@Service
public class DictionaryDataServiceImpl implements DictionaryDataService, ApplicationContextAware {private ApplicationContext applicationContext;private DictionaryService dictionaryService;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) {this.applicationContext = applicationContext;}private DictionaryService getDictionaryService() {if (dictionaryService == null) {dictionaryService = applicationContext.getBean(DictionaryService.class);}return dictionaryService;}public void someMethod() {Dictionary dict = getDictionaryService().getById(1);}
}
實現方式二:SpringContextHolder工具類
// 工具類
@Component
public class SpringContextHolder implements ApplicationContextAware {private static ApplicationContext context;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) {context = applicationContext;}public static <T> T getBean(Class<T> beanClass) {return context.getBean(beanClass);}public static <T> T getBean(String beanName, Class<T> beanClass) {return context.getBean(beanName, beanClass);}
}// 業務類使用
@Service
public class DictionaryDataServiceImpl implements DictionaryDataService {public void someMethod() {DictionaryService dictionaryService = SpringContextHolder.getBean(DictionaryService.class);Dictionary dict = dictionaryService.getById(1);}
}
優點:
- 完全避免了循環依賴
- 可以動態獲取bean
- 適合復雜的依賴場景
缺點:
- 失去了依賴注入的便利性
- 代碼可讀性稍差
- 增加了與Spring框架的耦合
4. 接口隔離 / 中間層解耦(侵入性高,推薦長期方案)
適用場景:重構現有架構,從根本上解決循環依賴問題
方案一:提取公共服務
// 提取公共邏輯到新的服務
@Service
public class DictionaryCommonService {@Resourceprivate DictionaryMapper dictionaryMapper;@Resource private DictionaryDataMapper dictionaryDataMapper;public Dictionary findDictionaryById(Integer id) {return dictionaryMapper.selectById(id);}public List<DictionaryData> findDatasByDictId(Integer dictId) {return dictionaryDataMapper.selectByDictId(dictId);}
}// 重構后的服務
@Service
public class DictionaryServiceImpl implements DictionaryService {@Resourceprivate DictionaryCommonService dictionaryCommonService;@Overridepublic JsonResult articleTypeList() {// 使用公共服務Dictionary dict = dictionaryCommonService.findDictionaryById(1);List<DictionaryData> dataList = dictionaryCommonService.findDatasByDictId(dict.getDictId());// 處理邏輯...}
}@Service
public class DictionaryDataServiceImpl implements DictionaryDataService {@Resourceprivate DictionaryCommonService dictionaryCommonService;// 業務邏輯使用公共服務
}
方案二:接口隔離原則
// 定義最小化接口
public interface DictionaryQueryService {Dictionary getById(Integer id);
}public interface DictionaryDataQueryService {List<DictionaryData> getByDictId(Integer dictId);
}// 實現類只依賴需要的接口
@Service
public class DictionaryServiceImpl implements DictionaryService, DictionaryQueryService {@Resourceprivate DictionaryDataQueryService dictionaryDataQueryService;// 實現邏輯...
}@Service
public class DictionaryDataServiceImpl implements DictionaryDataService, DictionaryDataQueryService {@Resourceprivate DictionaryQueryService dictionaryQueryService;// 實現邏輯...
}
優點:
- 從根本上解決了設計問題
- 提高了代碼的可維護性
- 符合SOLID原則
- 降低了模塊間的耦合度
缺點:
- 需要大量的代碼重構
- 可能涉及業務邏輯的調整
- 短期內工作量較大
5. 事件驅動(ApplicationEvent,侵入性中等,適合通知場景)
適用場景:一個服務需要通知另一個服務執行某些操作
實現方式:
// 定義事件
public class DictionaryDataChangeEvent extends ApplicationEvent {private final String dictCode;public DictionaryDataChangeEvent(Object source, String dictCode) {super(source);this.dictCode = dictCode;}public String getDictCode() {return dictCode;}
}// 事件發布者
@Service
public class DictionaryDataServiceImpl implements DictionaryDataService {@Resourceprivate ApplicationEventPublisher eventPublisher;@Overridepublic boolean save(DictionaryData entity) {boolean success = super.save(entity);if (success) {// 發布事件而不是直接調用其他服務eventPublisher.publishEvent(new DictionaryDataChangeEvent(this, entity.getDictCode()));}return success;}
}// 事件監聽者
@Service
public class DictionaryServiceImpl implements DictionaryService {@EventListenerpublic void handleDictionaryDataChange(DictionaryDataChangeEvent event) {// 處理字典數據變更后的邏輯String dictCode = event.getDictCode();// 清除緩存、更新狀態等}
}
優點:
- 完全解耦了兩個服務
- 支持異步處理
- 便于擴展(多個監聽者)
- 符合事件驅動架構
缺點:
- 增加了系統復雜度
- 調試相對困難
- 需要理解事件驅動模式
總結
方案 | 侵入性 | 適用場景 | 推薦指數 | 備注 |
---|---|---|---|---|
配置開啟循環依賴 | 最低 | 快速修復、臨時方案 | ?? | 治標不治本 |
@Lazy注解 | 低 | 簡單雙向依賴 | ???? | 優先推薦 |
手動獲取bean | 中等 | 復雜依賴關系 | ??? | 失去注入便利性 |
接口隔離/中間層 | 高 | 架構重構 | ????? | 長期最佳方案 |
事件驅動 | 中等 | 通知場景 | ???? | 解耦效果好 |
建議處理流程:
- 短期:使用
@Lazy
快速解決啟動問題 - 中期:分析業務邏輯,評估是否需要重構
- 長期:通過接口隔離或中間層徹底解決循環依賴
版本差異補充
Spring Boot 2.5.x 及之前:
- 默認
allow-circular-references: true
- 三級緩存自動處理循環依賴
- 開發者通常不會意識到循環依賴問題
Spring Boot 2.6.0+:
- 默認
allow-circular-references: false
- 啟動時檢查并報錯
- 強制開發者關注和解決循環依賴
Spring Boot 3.0+:
- 繼續保持對循環依賴的嚴格控制
- 進一步鼓勵良好的設計模式
- 可能在未來版本中完全移除循環依賴支持
這個變更體現了Spring團隊對代碼質量和架構設計的重視,雖然短期內會帶來一些遷移成本,但長期來看有利于項目的可維護性和穩定性。