Spring解决循环依赖
一、什么是循环依赖?
循环依赖(Circular Dependency) 指两个或多个组件相互直接或间接依赖,形成闭环引用关系。在Spring框架中,主要表现为Bean之间的相互注入。
1. 循环依赖的三种典型形态
// 案例1:直接循环依赖(A↔B)
@Component
class A {
@Autowired
private B b;
}
@Component
class B {
@Autowired
private A a;
}
// 案例2:间接循环依赖(A→B→C→A)
@Component
class C {
@Autowired
private A a;
}
// 案例3:自我依赖(极端情况)
@Component
class Self {
@Autowired
private Self self; // 实际开发中应避免
}
2. 循环依赖的危害
- 启动失败:Spring容器初始化时可能抛出
BeanCurrentlyInCreationException
- 代码耦合:破坏模块化设计原则
- 调试困难:难以追踪依赖关系
二、Spring的解决之道:三级缓存机制
1. 三级缓存结构
Spring通过三级缓存解决单例Bean的循环依赖问题:
缓存名称 | 数据结构 | 存储内容 |
---|---|---|
singletonObjects(一级缓存) | ConcurrentHashMap | 完全初始化好的Bean |
earlySingletonObjects(二级缓存) | HashMap | 早期暴露对象(未完成属性注入) |
singletonFactories(三级缓存) | HashMap | 对象工厂(用于生成早期对象) |
2. 核心解决流程
以A和B相互依赖为例:
三、源码级解析
1. 核心源码位置
DefaultSingletonBeanRegistry
:管理三级缓存AbstractAutowireCapableBeanFactory
:Bean创建主流程
2. 关键代码片段
// 创建Bean的核心方法
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. 实例化对象(此时对象属性为空)
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
// 2. 将ObjectFactory加入三级缓存(解决循环依赖的关键)
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 3. 属性注入(可能触发依赖Bean的创建)
populateBean(beanName, mbd, instanceWrapper);
// 4. 初始化
exposedObject = initializeBean(beanName, exposedObject, mbd);
return exposedObject;
}
// 三级缓存写入逻辑
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);
}
}
}
四、构造器注入 vs 属性注入
1. 构造器注入无法解决循环依赖
@Component
class C {
private D d;
@Autowired
public C(D d) { this.d = d; } // 构造器注入
}
@Component
class D {
private C c;
@Autowired
public D(C c) { this.c = c; } // 抛出BeanCurrentlyInCreationException
}
2. 原因分析
- 构造器注入发生在实例化阶段,此时对象尚未创建完成,无法放入三级缓存
- 属性注入发生在实例化之后,此时已通过三级缓存暴露早期引用
五、解决方案的局限性
1. 作用域限制
作用域类型 | 是否支持循环依赖 | 原因 |
---|---|---|
Singleton(默认) | ✅ | 三级缓存机制支持 |
Prototype | ❌ | 每次获取新对象,无法提前暴露 |
Request/Session | ❌ | 作用域生命周期短且不固定 |
2. 代理对象问题
- 若Bean需要AOP代理,必须使用CGLIB代理(JDK代理需要接口)
- 解决方法:配置
spring.aop.proxy-target-class=true
六、开发建议与最佳实践
1. 避免循环依赖的方法
- 模块化设计:遵循单一职责原则
- 依赖方向控制:保持单向依赖(A→B→C)
- 接口抽象:通过接口解耦具体实现
- 延迟加载:使用
@Lazy
注解
@Component
class E {
@Lazy // 延迟注入
@Autowired
private F f;
}
2. 必须使用循环依赖时
- 确保使用属性注入(setter/字段注入)
- 所有相关Bean必须为单例
- 避免在
@PostConstruct
方法中访问依赖Bean
七、终极解决方案:重新设计
当遇到复杂的循环依赖时,应考虑以下重构方案:
重构方法 | 实施案例 |
---|---|
引入第三方类 | 将公共逻辑提取到新类C,让A、B都依赖C |
使用事件驱动 | 通过ApplicationEvent解耦 |
转换为方法调用 | 使用ApplicationContext.getBean() |
接口分层 | 定义服务接口与实现类分离 |
总结
Spring通过三级缓存机制巧妙地解决了单例Bean的循环依赖问题,但开发者应认识到:
- 这是框架层面的补救措施,而非设计推荐
- 构造器注入的循环依赖无法解决
- 原型作用域的Bean不支持循环依赖
- 复杂项目应通过代码重构消除循环依赖