1. 循环依赖现象解析
在Spring框架开发中,我们经常会遇到两个或多个Bean相互依赖的情况。比如Bean A依赖Bean B,同时Bean B又依赖Bean A,这就形成了典型的循环依赖场景。这种设计在业务逻辑中其实很常见,比如订单服务需要调用用户服务获取用户信息,而用户服务又需要调用订单服务获取用户的历史订单记录。
Spring容器在启动时,会按照一定的顺序初始化Bean。当遇到循环依赖时,如果处理不当就会抛出BeanCurrentlyInCreationException异常。但有趣的是,Spring其实已经内置了对循环依赖的处理机制,只是这个机制有一定的局限性。
2. Spring解决循环依赖的底层原理
2.1 三级缓存机制
Spring通过三级缓存来解决循环依赖问题:
- 一级缓存(singletonObjects):存放完全初始化好的Bean
- 二级缓存(earlySingletonObjects):存放原始Bean对象(尚未填充属性)
- 三级缓存(singletonFactories):存放Bean工厂对象
当Bean A开始初始化时:
- 首先通过构造器创建原始对象A
- 将对象A放入三级缓存
- 开始注入A的依赖项,发现需要Bean B
- 开始初始化Bean B
- Bean B初始化过程中又发现需要Bean A
- 此时从三级缓存中获取到A的工厂对象,生成早期引用
- Bean B完成初始化
- Bean A注入B完成,最终完成初始化
2.2 构造器注入的限制
需要注意的是,Spring只能解决通过setter方法或字段注入方式形成的循环依赖。如果是通过构造器注入形成的循环依赖,Spring是无法解决的。这是因为:
- setter注入是在对象实例化之后进行的
- 构造器注入必须在实例化时完成,此时对象还未完全创建,无法放入缓存
3. 循环依赖的实战处理方案
3.1 推荐解决方案
-
重新设计架构:考虑是否真的需要循环依赖,能否通过接口抽象、事件机制等方式解耦
-
使用@Lazy注解:
java复制@Service
public class ServiceA {
@Autowired
@Lazy
private ServiceB serviceB;
}
-
使用Setter注入替代构造器注入
-
应用上下文感知:
java复制@Service
public class ServiceA implements ApplicationContextAware {
private ApplicationContext context;
public void setApplicationContext(ApplicationContext ctx) {
this.context = ctx;
}
public void doSomething() {
ServiceB serviceB = context.getBean(ServiceB.class);
// 使用serviceB
}
}
3.2 不推荐的解决方案
- 使用@DependsOn强制指定初始化顺序(容易导致更复杂的问题)
- 使用静态方法获取Bean(破坏IOC容器的设计原则)
- 在构造函数中调用业务方法(容易导致NPE)
4. 循环依赖的调试技巧
当遇到循环依赖问题时,可以通过以下方式调试:
- 开启Spring调试日志:
properties复制logging.level.org.springframework.beans=DEBUG
- 分析Bean创建堆栈:
- 查找BeanCurrentlyInCreationException异常
- 查看异常信息中提到的循环链
- 使用断点调试:
- 在AbstractAutowireCapableBeanFactory的doCreateBean方法设置断点
- 观察三级缓存的变化情况
- 可视化工具:
- 使用Spring Boot Actuator的/beans端点
- 使用IDEA的Spring Beans依赖图
5. 性能影响与最佳实践
循环依赖虽然能被Spring解决,但会带来一定的性能开销:
- 初始化时间增加:需要多次访问缓存
- 内存占用增加:需要维护额外的缓存
- 调试难度增加:调用链路变得复杂
最佳实践建议:
- 尽量避免循环依赖,保持单向依赖
- 如果必须循环依赖,尽量使用接口抽象
- 将循环依赖限制在最小范围内
- 添加清晰的文档说明
- 编写单元测试验证循环依赖场景
6. 特殊场景处理
6.1 AOP代理下的循环依赖
当Bean被AOP代理时,循环依赖的处理会更加复杂。Spring通过SmartInstantiationAwareBeanPostProcessor来处理这种情况,确保返回的是代理对象而非原始对象。
6.2 原型作用域的循环依赖
Spring无法解决原型作用域(prototype)的循环依赖,因为:
- 原型Bean每次都会创建新实例
- 无法通过缓存机制解决
- 会导致无限递归创建对象
解决方案:
- 改为单例作用域
- 使用Provider延迟获取:
java复制@Service
public class ServiceA {
@Autowired
private Provider<ServiceB> serviceBProvider;
public void method() {
ServiceB serviceB = serviceBProvider.get();
// 使用serviceB
}
}
7. Spring Boot中的循环依赖
Spring Boot 2.6.0之后默认禁止了循环依赖,需要在配置中显式开启:
properties复制spring.main.allow-circular-references=true
这个改变是为了促使开发者编写更健康的代码结构。如果升级后遇到循环依赖问题,可以考虑:
- 按照错误提示重构代码
- 临时开启上述配置(不推荐长期使用)
- 使用@Lazy注解标记循环依赖点
8. 常见问题排查
-
错误:Requested bean is currently in creation
- 检查是否是构造器注入导致的循环依赖
- 确认是否有多个@Configuration类相互@Autowired
-
错误:BeanCreationException with cycle detected
- 使用--debug模式启动查看详细循环链
- 检查是否有意外的循环依赖(如通过@PostConstruct方法)
-
代理对象行为异常
- 确认是否因为循环依赖导致AOP代理异常
- 检查@Async等其他代理机制是否干扰
-
单元测试失败
- 测试环境可能需要显式设置allow-circular-references
- 考虑使用@MockBean打破测试中的循环依赖