1. Spring循环依赖问题本质剖析
循环依赖在Spring应用开发中是个高频出现的"老大难"问题。当Bean A依赖Bean B,同时Bean B又反过来依赖Bean A时,就形成了典型的循环引用场景。这种设计在业务系统中其实很常见,比如订单服务需要调用支付服务,而支付服务又需要验证订单信息。
Spring容器默认是支持解决单例作用域下的循环依赖的,但原型(prototype)作用域的循环依赖会直接抛出BeanCurrentlyInCreationException。这是因为单例Bean在Spring容器中有完整的生命周期管理机制,而原型Bean每次获取都是新实例,无法通过提前暴露引用的方式解决循环依赖。
关键理解:Spring解决循环依赖的核心在于"提前暴露"机制。当Bean还在创建过程中时,Spring就允许其他Bean引用它的半成品状态。
2. 三级缓存机制深度拆解
Spring通过三级缓存完美解决了循环依赖问题,这三层缓存分别是:
- singletonObjects(一级缓存):存放完全初始化好的Bean
- earlySingletonObjects(二级缓存):存放提前暴露的原始Bean(尚未填充属性)
- singletonFactories(三级缓存):存放Bean工厂对象,用于生成原始Bean
具体工作流程如下:
- 创建Bean A时,首先调用构造器实例化对象
- 将刚实例化但未初始化的Bean A放入三级缓存(此时是原始对象)
- 开始为Bean A填充属性时,发现需要注入Bean B
- 创建Bean B时同样走这个流程,当Bean B需要注入Bean A时
- 从三级缓存中获取Bean A的早期引用(通过ObjectFactory.getObject())
- Bean B完成创建后放入一级缓存
- Bean A继续完成属性注入,最终也放入一级缓存
java复制// 典型的三级缓存获取逻辑
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
3. 构造器注入的循环依赖难题
虽然Spring能解决属性注入的循环依赖,但对于构造器注入的循环依赖却无能为力。这是因为:
- 属性注入:先调用无参构造器创建实例,再通过反射设置属性
- 构造器注入:必须在创建时就传入完整的依赖对象
当使用构造器注入时,Bean A的创建需要先有Bean B,而Bean B的创建又需要先有Bean A,这就形成了死锁。Spring会直接抛出BeanCurrentlyInCreationException异常。
解决方案通常有:
- 改用属性注入(setter注入)
- 使用@Lazy延迟加载其中一个Bean
- 重构代码设计,打破循环依赖
4. 循环依赖的实战应对策略
在实际项目中,遇到循环依赖时可以考虑以下解决方案:
方案一:代码重构(推荐)
- 提取公共逻辑到第三个Bean中
- 使用事件驱动架构解耦
- 采用门面模式封装相互调用的逻辑
方案二:技术手段
java复制// 使用@Lazy注解延迟加载
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(@Lazy PaymentService paymentService) {
this.paymentService = paymentService;
}
}
方案三:调整注入方式
- 将构造器注入改为属性注入
- 使用ApplicationContextAware手动获取Bean
5. 循环依赖的典型问题排查
问题现象一:BeanCurrentlyInCreationException异常
- 检查是否是prototype作用域的循环依赖
- 确认是否使用了构造器注入
- 查看异常信息中的Bean依赖链
问题现象二:注入的对象为null
- 检查三级缓存是否正常工作
- 确认Bean是否被正确扫描到
- 查看AOP代理是否影响了Bean的创建
调试技巧:
- 开启Spring调试日志:logging.level.org.springframework.beans=DEBUG
- 在DefaultSingletonBeanRegistry类中设置断点
- 观察三级缓存的变化情况
6. 高级场景下的循环依赖处理
当引入AOP代理时,循环依赖的处理会更加复杂。Spring通过SmartInstantiationAwareBeanPostProcessor来解决这个问题,关键实现类是AbstractAutoProxyCreator。
代理对象的创建流程:
- 原始对象先被放入三级缓存
- 当其他Bean注入该Bean时,会通过BeanPostProcessor创建代理对象
- 最终放入一级缓存的是代理对象而非原始对象
这种机制保证了AOP增强不会破坏循环依赖的解决。
对于@Async等基于代理的注解,同样适用这个原理。但需要注意:
- 确保@Async方法都是public的
- 同一个类内的自调用不会触发代理逻辑
- 代理对象的初始化时机可能影响依赖注入的顺序
7. Spring Boot中的特殊处理
Spring Boot在自动配置阶段也可能遇到循环依赖问题。其解决方案包括:
- 使用@DependsOn明确指定Bean的创建顺序
- 配置spring.main.allow-circular-references=true(不推荐)
- 合理使用@AutoConfigureAfter/@AutoConfigureBefore
一个典型的自动配置循环依赖案例是:
- DataSourceAutoConfiguration 需要 JdbcTemplate
- JdbcTemplateAutoConfiguration 需要 DataSource
Spring Boot通过自动配置的排序机制避免了这类问题。
8. 性能优化与最佳实践
循环依赖虽然能被解决,但会带来一定的性能开销:
- 增加了Bean的创建步骤
- 需要额外的缓存管理
- 可能影响启动速度
优化建议:
- 尽量避免循环依赖,保持单向依赖
- 对于必须的循环依赖,使用接口抽象
- 监控Bean的创建时间,重点关注复杂依赖链
在大型项目中,可以通过ArchUnit等工具静态检查循环依赖:
java复制@ArchTest
static final ArchRule no_cycles =
slices().matching("com.your.package.(*)")
.should().beFreeOfCycles();
9. 源码级深度解析
理解AbstractAutowireCapableBeanFactory的doCreateBean方法是掌握循环依赖的关键:
- createBeanInstance:通过构造器创建原始对象
- addSingletonFactory:将Bean工厂加入三级缓存
- populateBean:填充属性(可能触发依赖Bean的创建)
- initializeBean:执行初始化回调
关键源码片段:
java复制// 提前暴露引用的核心代码
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
10. 其他容器对循环依赖的处理
对比其他IoC容器对循环依赖的处理方式:
| 容器名称 | 循环依赖支持 | 实现机制 |
|---|---|---|
| Spring | 支持单例Bean | 三级缓存 |
| Guice | 有限支持 | Provider延迟注入 |
| Dagger | 完全禁止 | 编译期检查 |
| Micronaut | 支持 | 类似Spring的三级缓存 |
在微服务架构下,可以考虑将循环依赖提升到服务层面,通过:
- 异步消息队列
- 事件溯源模式
- CQRS架构
这些方案能更好地解决分布式环境下的循环调用问题。
