1. 循环依赖问题背景与核心概念
在Spring Boot应用开发中,Bean的创建过程是整个框架运转的核心机制之一。工厂方法作为一种灵活的Bean创建方式,允许开发者通过@Bean注解标注的方法来定义复杂的对象实例化逻辑。但在实际开发中,当两个或多个Bean通过工厂方法相互依赖时,就会形成循环依赖(Circular Dependency)的典型场景。
循环依赖本质上是指两个或多个组件相互直接或间接引用,形成闭环依赖关系。在Spring框架中,这种依赖关系可能出现在以下三种场景:
- 构造器注入循环依赖(Spring无法解决)
- Setter方法注入循环依赖(Spring通过三级缓存机制解决)
- 工厂方法创建的Bean之间的循环依赖(本文重点讨论场景)
工厂方法模式下的循环依赖相比普通Bean的循环依赖更为复杂,因为涉及到方法调用的时序问题。Spring容器在初始化时,会按照特定的顺序调用这些工厂方法,如果设计不当就会抛出著名的BeanCurrentlyInCreationException异常。
2. 工厂方法创建Bean的机制剖析
2.1 Spring Bean生命周期中的工厂方法阶段
当Spring容器遇到@Bean注解的方法时,会将其视为一个工厂方法,并在适当的时机调用该方法来生成Bean实例。这个过程的特殊性在于:
- 方法调用时机:工厂方法不是在Bean定义加载时立即执行,而是在第一次被依赖时触发
- 代理机制:Spring会对工厂方法进行特殊处理,可能生成CGLIB代理
- 依赖解析:方法参数会被自动装配,这成为循环依赖的潜在入口点
java复制@Configuration
public class AppConfig {
@Bean
public ServiceA serviceA(ServiceB serviceB) {
return new ServiceA(serviceB);
}
@Bean
public ServiceB serviceB(ServiceA serviceA) {
return new ServiceB(serviceA);
}
}
2.2 三级缓存的工作机制
Spring解决循环依赖的核心在于三级缓存设计:
- singletonObjects:一级缓存,存放完全初始化好的Bean
- earlySingletonObjects:二级缓存,存放原始Bean对象(尚未填充属性)
- singletonFactories:三级缓存,存放ObjectFactory对象
对于工厂方法创建的Bean,其处理流程有所不同:
- 普通Bean:实例化→放入三级缓存→填充属性→初始化→移入一级缓存
- 工厂方法Bean:方法调用前会先创建一个工厂对象放入三级缓存
3. 工厂方法循环依赖的典型场景分析
3.1 直接循环依赖案例
考虑以下配置类会引发典型的循环依赖问题:
java复制@Configuration
public class CircularConfig {
@Bean
public CircularA circularA(CircularB circularB) {
return new CircularA(circularB);
}
@Bean
public CircularB circularB(CircularA circularA) {
return new CircularB(circularA);
}
}
Spring处理这个场景的详细步骤如下:
- 开始创建circularA,发现需要circularB
- 暂停circularA的创建,开始创建circularB
- 创建circularB时发现需要circularA
- 此时Spring会从三级缓存中获取circularA的早期引用
- 将早期引用注入circularB,完成circularB的创建
- 将创建好的circularB注入circularA,完成circularA的创建
3.2 间接循环依赖案例
更复杂的场景可能涉及多个Bean的链式依赖:
java复制@Configuration
public class ChainConfig {
@Bean
public ChainA chainA(ChainB chainB) {
return new ChainA(chainB);
}
@Bean
public ChainB chainB(ChainC chainC) {
return new ChainB(chainC);
}
@Bean
public ChainC chainC(ChainA chainA) {
return new ChainC(chainA);
}
}
这种场景下Spring仍然能够处理,但依赖链越长,出现问题的风险越高。
4. 解决方案与最佳实践
4.1 架构层面解决方案
-
重新设计依赖关系:
- 引入中间层接口
- 使用事件驱动模式解耦
- 考虑领域驱动设计中的聚合根概念
-
模块化拆分:
- 将相互依赖的功能拆分到不同模块
- 定义清晰的模块边界
4.2 技术层面解决方案
- @Lazy注解的使用:
java复制@Bean
public ServiceA serviceA(@Lazy ServiceB serviceB) {
return new ServiceA(serviceB);
}
- Setter/Field注入替代构造器注入:
java复制@Configuration
public class SolutionConfig {
@Bean
public ServiceA serviceA() {
ServiceA serviceA = new ServiceA();
serviceA.setServiceB(serviceB());
return serviceA;
}
@Bean
public ServiceB serviceB() {
ServiceB serviceB = new ServiceB();
serviceB.setServiceA(serviceA());
return serviceB;
}
}
- 使用ObjectProvider延迟注入:
java复制@Bean
public ServiceA serviceA(ObjectProvider<ServiceB> serviceBProvider) {
return new ServiceA(serviceBProvider.getIfAvailable());
}
4.3 Spring内部机制调优
- 调整Bean初始化顺序:
java复制@DependsOn("serviceB")
@Bean
public ServiceA serviceA(ServiceB serviceB) {
// ...
}
- 自定义SmartInitializingSingleton实现:
java复制public class MyInitializer implements SmartInitializingSingleton {
@Override
public void afterSingletonsInstantiated() {
// 在此处理依赖关系
}
}
5. 深度排查与问题诊断
5.1 异常分析指南
当遇到BeanCurrentlyInCreationException时,可按以下步骤排查:
- 分析异常堆栈中的Bean创建链
- 检查是否有构造器注入的循环依赖
- 确认工厂方法的参数是否形成闭环
- 检查@DependsOn注解的使用情况
5.2 调试技巧
- 启用Spring调试日志:
properties复制logging.level.org.springframework.beans=DEBUG
- 使用BeanPostProcessor跟踪:
java复制public class TracingPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("Creating: " + beanName);
return bean;
}
}
- 断点设置建议:
- AbstractAutowireCapableBeanFactory.doCreateBean()
- DefaultSingletonBeanRegistry.getSingleton()
5.3 性能影响评估
循环依赖解决方案对性能的影响主要体现在:
- 代理生成开销:CGLIB代理的创建成本
- 缓存查询开销:三级缓存的查找过程
- 初始化时序复杂度:Bean的初始化顺序管理
建议在性能敏感场景下进行基准测试,比较不同解决方案的耗时差异。
6. 高级应用场景
6.1 结合AOP代理的复杂场景
当循环依赖的Bean还需要AOP代理时,情况会变得更加复杂:
java复制@Configuration
@EnableAspectJAutoProxy
public class ProxyConfig {
@Bean
public Advice myAdvice() {
return new MyAdvice();
}
@Bean
public ServiceA serviceA(ServiceB serviceB) {
return new ServiceA(serviceB);
}
@Bean
public ServiceB serviceB(ServiceA serviceA) {
return new ServiceB(serviceA);
}
}
这种场景下Spring会使用额外的代理策略,可能导致以下问题:
- 代理链过长
- 方法调用栈加深
- 异常信息难以理解
6.2 条件化Bean定义中的循环依赖
当结合@Conditional注解时,循环依赖的处理需要特别注意:
java复制@Configuration
public class ConditionalConfig {
@Bean
@ConditionalOnBean(ServiceB.class)
public ServiceA serviceA(ServiceB serviceB) {
return new ServiceA(serviceB);
}
@Bean
@ConditionalOnBean(ServiceA.class)
public ServiceB serviceB(ServiceA serviceA) {
return new ServiceB(serviceA);
}
}
这种配置会导致两个Bean都无法满足对方的条件,形成死锁状态。解决方案包括:
- 使用@DependsOn明确依赖顺序
- 将条件判断移到更上层的配置中
- 使用配置属性驱动而非Bean存在性检查
7. 替代方案与模式比较
7.1 构造函数注入 vs Setter注入
在工厂方法场景下,两种注入方式的对比:
| 特性 | 构造函数注入 | Setter注入 |
|---|---|---|
| 循环依赖支持 | 不支持 | 支持 |
| 不变性 | 好(final字段) | 差 |
| 测试便利性 | 较好 | 优秀 |
| 代码简洁度 | 参数多时冗长 | 更灵活 |
7.2 工厂方法模式 vs 组件扫描
两种Bean定义方式的循环依赖处理能力对比:
-
组件扫描(@Component):
- Spring处理更自动化
- 代理生成更直接
- 调试信息更清晰
-
工厂方法(@Bean):
- 更灵活的实例化控制
- 适合集成第三方库
- 但循环依赖处理更复杂
7.3 Spring与其它框架的对比
不同IoC容器对工厂方法循环依赖的处理差异:
-
Guice:
- 完全禁止循环依赖
- 需要显式使用Provider接口
-
Dagger:
- 编译时检测循环依赖
- 提供更明确的错误信息
-
Micronaut:
- 类似Spring的解决方案
- 但基于编译时代理生成
8. 实战经验与性能优化
在实际项目中处理工厂方法循环依赖时,我总结出以下经验:
-
依赖方向优化:
- 保持依赖单向流动
- 识别真正的核心依赖方向
- 示例:用户服务→订单服务,不应反向依赖
-
延迟加载策略:
java复制@Bean
public ServiceA serviceA(ApplicationContext ctx) {
return new ServiceA(() -> ctx.getBean(ServiceB.class));
}
-
监控与告警:
- 使用Spring Boot Actuator监控Bean初始化时间
- 设置合理的超时阈值
- 对复杂依赖关系进行可视化展示
-
测试策略:
- 编写专门的循环依赖测试用例
- 使用@DirtiesContext确保测试隔离
- 集成测试中模拟依赖环
对于大型项目,建议建立架构规范:
- 明确禁止超过3层的依赖链
- 要求模块间通过接口通信
- 定期进行依赖关系审计