引言
在spring boot开发中,依赖注入(di)是核心特性之一,它帮助我们构建松耦合、可测试的应用程序。然而,当多个bean相互依赖时,可能会形成循环依赖(circular dependency),导致应用启动失败。
本文将通过一个实际错误案例,深入分析spring boot循环依赖的成因、解决方案,并提供最佳实践建议,帮助开发者避免此类问题。
1. 什么是循环依赖
1.1 循环依赖的定义
循环依赖指的是两个或多个bean相互依赖,形成一个闭环。例如:
servicea
依赖serviceb
serviceb
依赖servicec
servicec
又依赖servicea
这样就会形成一个循环链,spring在初始化时无法决定哪个bean应该先创建。
1.2 spring boot的默认行为
在spring boot 2.6+版本中,循环依赖默认被禁止,如果检测到循环依赖,会抛出如下错误:
application failed to start
*
description:
the dependencies of some of the beans in the application context form a cycle:
...
action:
relying upon circular references is discouraged and they are prohibited by default.
2. 案例分析:循环依赖的错误日志
以下是本文讨论的错误日志:
error starting applicationcontext. to display the conditions report re-run your application with 'debug' enabled.
*
application failed to start
*
description:
the dependencies of some of the beans in the application context form a cycle:
aftertestcontroller → aftertestservice → opmmediaflowcontrolservice → opmoperateteamservice → syschannelcompanyservice → opmchannelaccountservice → syschannelcompanyservice
依赖链分析:
aftertestcontroller
依赖aftertestservice
aftertestservice
依赖opmmediaflowcontrolservice
opmmediaflowcontrolservice
依赖opmoperateteamservice
opmoperateteamservice
依赖syschannelcompanyservice
syschannelcompanyservice
依赖opmchannelaccountservice
opmchannelaccountservice
又依赖syschannelcompanyservice
(形成闭环)
3. 解决方案
3.1 方案1:重构代码(推荐)
最佳实践是避免循环依赖,通常可以通过以下方式重构:
(1) 提取公共逻辑到新service
如果两个service需要互相调用,可以将公共逻辑提取到第三个service:
@service public class commonservice { // 公共方法 }
(2) 使用接口或事件驱动模式
接口分离:让service依赖接口,而不是具体实现。
事件驱动:使用spring的applicationevent
解耦:
@service public class servicea { @autowired private applicationeventpublisher eventpublisher; public void dosomething() { eventpublisher.publishevent(new customevent(data)); } } @component public class serviceb { @eventlistener public void handleevent(customevent event) { // 处理事件 } }
3.2 方案2:使用@lazy注解(次优方案)
如果暂时无法重构,可以在其中一个依赖上使用@lazy
,延迟初始化bean:
@service public class servicea { @lazy // 延迟注入 @autowired private serviceb serviceb; }
缺点:
- 只是延迟问题,而不是真正解决循环依赖。
- 可能导致运行时npe(nullpointerexception)。
3.3 方案3:允许循环依赖(临时方案)
如果必须保留循环依赖,可以在application.properties
中启用:
spring.main.allow-circular-references=true
缺点:
- 只是绕过问题,可能导致不可预见的初始化顺序问题。
- 不推荐在生产环境使用。
4. 深入理解spring的循环依赖处理机制
4.1 spring的三级缓存
spring通过三级缓存解决部分循环依赖问题:
- singleton objects(一级缓存):存放完全初始化好的bean。
- early singleton objects(二级缓存):存放半成品bean(已实例化但未初始化)。
- singleton factories(三级缓存):存放bean工厂,用于生成代理对象。
4.2 循环依赖的解决条件
- 仅适用于单例(singleton)作用域的bean。
- 仅适用于字段注入(@autowired)或setter注入,不适用于构造器注入。
5. 最佳实践总结
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
重构代码 | 长期项目 | 彻底解决问题,代码更清晰 | 需要设计调整 |
@lazy注解 | 短期修复 | 简单快捷 | 可能隐藏问题 |
允许循环依赖 | 紧急修复 | 快速绕过问题 | 不推荐,可能导致未知错误 |
推荐做法:
- 避免双向依赖,尽量采用单向依赖(controller → service → repository)。
- 提取公共逻辑到新service或utils类。
- 使用事件驱动(
applicationevent
)解耦service。 - 尽量使用构造器注入,避免字段注入(能提前发现循环依赖问题)。
6. 示例代码:重构后的结构
6.1 原结构(循环依赖)
@service public class servicea { @autowired private serviceb serviceb; } @service public class serviceb { @autowired private servicea servicea; }
6.2 重构后(解耦)
// 提取公共逻辑到新service @service public class commonservice { // 公共方法 } // servicea 依赖 commonservice @service public class servicea { @autowired private commonservice commonservice; } // serviceb 依赖 commonservice @service public class serviceb { @autowired private commonservice commonservice; }
7. 结论
循环依赖是spring boot开发中的常见问题,通常表明设计上存在优化空间。虽然可以通过@lazy
或allow-circular-references
临时解决,但重构代码才是最佳实践。
关键点总结:
- 避免双向依赖,尽量保持单向依赖链。
- 优先使用构造器注入,能更早发现循环依赖问题。
- 提取公共逻辑或使用事件驱动解耦service。
- 不要滥用
@lazy
或allow-circular-references
,它们只是临时解决方案。
通过合理设计,我们可以构建更健壮、可维护的spring boot应用!
以上就是springboot中循环依赖问题的原理与解决方案的详细内容,更多关于springboot循环依赖问题的资料请关注代码网其它相关文章!
发表评论