在Spring框架中,循环依赖是指两个或多个Bean之间相互依赖,形成一个闭环。例如,A依赖B,B又依赖A。这种情况如果不妥善处理,会导致Bean创建过程陷入无限循环,最终引发BeanCurrentlyInCreationException异常。作为资深Java开发工程师,回答这个问题时需要从原理、解决方案、源码层面以及实际应用场景等多个维度进行全面阐述。以下是一个结构化的回答框架:
一、循环依赖的本质与类型
本质:
Spring的Bean生命周期中,默认情况下是创建(实例化) -> 属性填充(依赖注入) -> 初始化(调用初始化方法)。当两个Bean相互依赖时,可能在属性填充阶段形成死循环。
常见类型:
- 构造器循环依赖:通过构造函数注入形成的循环依赖(无法解决,必须避免)。
- Setter循环依赖:通过Setter方法注入形成的循环依赖(Spring默认解决)。
- 字段循环依赖:通过字段注入(@Autowired)形成的循环依赖(Spring默认解决)。
二、Spring解决循环依赖的核心机制
Spring通过三级缓存机制解决循环依赖,核心在于提前暴露未完全初始化的Bean。三级缓存的定义如下:
- singletonObjects(一级缓存):存储完全初始化且可用的单例Bean。
- singletonFactories(三级缓存):存储创建Bean的工厂(ObjectFactory),用于提前暴露Bean。
- earlySingletonObjects(二级缓存):存储提前暴露的半成品Bean(已实例化但未完全初始化)。
关键步骤:
- 实例化阶段:当创建Bean A时,先通过构造函数实例化A,此时A是一个半成品(属性未填充)。
- 提前暴露:将A的ObjectFactory放入三级缓存(singletonFactories),允许其他Bean提前引用。
- 依赖注入:在填充A的属性时发现依赖B,转去创建B。
- B的依赖处理:创建B时发现依赖A,从三级缓存获取A的ObjectFactory,通过它获取A的半成品引用,放入二级缓存(earlySingletonObjects),完成B的创建。
- 完成A的创建:B创建完成后,继续完成A的属性填充和初始化,最终将A从二级缓存移至一级缓存。
三、源码层面的实现细节
关键类:DefaultSingletonBeanRegistry
核心方法:
getSingleton(String beanName, boolean allowEarlyReference):获取单例Bean的入口方法。addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory):将Bean的工厂添加到三级缓存。beforeSingletonCreation(String beanName):标记Bean正在创建中。afterSingletonCreation(String beanName):标记Bean创建完成。
源码流程简化:
// 获取单例Bean的核心逻辑
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
// 2. 如果Bean正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 3. 从二级缓存获取
singletonObject = this.earlySingletonObjects.get(beanName);
// 4. 二级缓存没有且允许提前引用
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 5. 从三级缓存获取工厂
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 6. 通过工厂获取半成品Bean
singletonObject = singletonFactory.getObject();
// 7. 放入二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 8. 从三级缓存移除
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
四、构造器循环依赖为何无法解决?
构造器循环依赖发生在实例化阶段,而Spring的三级缓存机制是在实例化之后才发挥作用。当通过构造函数注入依赖时,必须先获取依赖的Bean,导致两个Bean互相等待对方创建,形成死锁。
示例代码:
public class A {
private B b;
public A(B b) { this.b = b; } // 构造器依赖B
}
public class B {
private A a;
public B(A a) { this.a = a; } // 构造器依赖A
}
解决建议:
- 重构设计,避免循环依赖(推荐)。
- 使用Setter/字段注入替代构造器注入。
五、实际应用中的最佳实践
-
优先避免循环依赖:
- 重构代码,提取公共服务或使用中介模式解耦。
- 检查设计是否合理,是否违反单一职责原则。
-
合理使用注入方式:
- 构造器注入:强制依赖,适用于必需的依赖关系。
- Setter/字段注入:允许延迟注入,可解决循环依赖。
-
使用@Lazy注解:
@Service public class A { @Autowired @Lazy // 延迟加载B private B b; } -
编程式注入:
@Service public class A { private B b; @Autowired public void setApplicationContext(ApplicationContext context) { this.b = context.getBean(B.class); // 编程式获取B } }
六、面试回答逻辑总结
- 定义与类型:明确循环依赖的概念和常见类型,指出构造器循环依赖无法解决。
- 核心机制:详细解释三级缓存的作用和工作流程,强调提前暴露半成品Bean的关键思想。
- 源码分析:提及关键类和方法,展示对Spring底层实现的理解。
- 最佳实践:结合实际项目经验,给出避免和解决循环依赖的具体建议。
- 扩展思考:讨论循环依赖对系统设计的影响,以及如何从架构层面减少依赖耦合。
通过这种结构化的回答,既能展示技术深度(源码理解),又能体现工程经验(最佳实践),符合资深工程师的能力要求。

3873

被折叠的 条评论
为什么被折叠?



