
在Spring Boot应用开发中,循环依赖(Circular Dependency) 是一个常见却容易被忽视的问题。从Spring Boot 2.6版本开始,框架默认禁止循环依赖,启动时如果检测到循环引用就会直接抛出异常,导致应用启动失败。这项改动在社区引起了较大讨论,许多老项目升级后突然"无法启动"。
那么,Spring Boot为什么要这么做?循环依赖到底有什么危害?本文将从原理、原因、代码示例和解决方案多个角度进行详细说明。 [1] [2]
什么是循环依赖?
循环依赖是指两个或多个Bean之间相互依赖,形成一个闭环。例如:
- Bean A 依赖 Bean B
- Bean B 依赖 Bean A
Spring IoC容器在创建Bean时需要按照依赖顺序进行实例化。当出现循环时,容器无法决定"先创建谁",从而产生问题。
Spring如何处理依赖注入?
Spring主要支持三种依赖注入方式:
- 构造器注入(Constructor Injection)------ 推荐方式
- Setter注入
- 字段注入(@Autowired on field)
在早期Spring版本中:
- 构造器注入的循环依赖会直接失败(BeanCurrentlyInCreationException)。
- Setter/字段注入的循环依赖,Spring通过三级缓存 机制(singletonObjects、earlySingletonObjects、singletonFactories)可以部分解决:提前暴露一个未完全初始化的Bean代理,从而打破循环。 [3]
但这种"自动解决"实际上掩盖了设计问题。
Spring Boot禁止循环依赖的原因
-
代码设计问题(Code Smell)
循环依赖通常意味着类之间职责划分不清晰、耦合度过高,违反了单一职责原则(SRP) 和 依赖倒置原则(DIP)。长期积累会导致代码难以维护、测试困难。
-
潜在的运行时问题
即使Spring通过代理解决了初始化,运行时仍可能出现:
- 未完全初始化的Bean被使用导致NullPointerException或状态不一致。
- 内存泄漏风险。
- 难以预测的行为,尤其在并发或复杂生命周期场景下。
-
Fail Fast原则
尽早暴露问题比让应用"勉强启动"然后在生产环境中出问题更好。Spring Boot团队希望开发者主动重构代码,而不是依赖框架的"容错"机制。 [1]
-
推动架构优化
禁止循环依赖鼓励使用事件驱动、引入中间层、接口抽象等更优雅的设计模式,提升系统可扩展性和可测试性。
-
历史包袱清理
早期允许循环依赖导致许多项目积累了坏味道。默认禁止能迫使团队清理技术债。
代码示例:循环依赖的演示
示例1:构造器注入循环依赖(一定会失败)
java
// ServiceA.java
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { // 构造器注入
this.serviceB = serviceB;
}
public void doSomething() {
serviceB.doSomething();
}
}
// ServiceB.java
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { // 构造器注入
this.serviceA = serviceA;
}
public void doSomething() {
serviceA.doSomething();
}
}
启动应用会报错:
BeanCurrentlyInCreationException: Error creating bean with name 'serviceA':
Requested bean is currently in creation: Is there an unresolvable circular reference?
示例2:字段注入循环依赖(早期版本可通过,2.6+默认禁止)
java
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // 字段注入
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA; // 字段注入
}
如何解决循环依赖?
推荐方案(治本):
-
重构代码
- 提取公共接口或服务。
- 使用事件驱动(ApplicationEventPublisher)。
- 引入第三方中介类或Facade模式。
- 拆分服务职责。
-
@Lazy注解(临时方案)
java@Service public class ServiceA { private final ServiceB serviceB; public ServiceA(@Lazy ServiceB serviceB) { this.serviceB = serviceB; } } -
Setter注入(不推荐作为长期方案)
-
配置允许循环依赖(强烈不推荐,仅用于紧急过渡)
properties# application.properties spring.main.allow-circular-references=true
最佳实践建议
- 优先使用构造器注入:它能及早发现循环依赖问题。
- 定期进行架构审查,使用工具如ArchUnit检测循环依赖。
- 微服务架构中,避免跨服务循环调用,可通过消息队列解耦。
- 新项目从一开始就严格避免循环依赖。
最后小结哈
Spring Boot禁止循环依赖不是技术限制,而是框架对开发者负责的表现。它迫使我们写出更干净、可维护的代码。虽然短期内可能需要重构,但从长远看,这大大提升了系统的稳定性和可扩展性。 [4]
当你看到BeanCurrentlyInCreationException时,不要急着开启allow-circular-references,而是借此机会审视你的服务设计------这往往是改进架构的绝佳机会。