本站所有源码均为自动秒发货,默认(百度网盘)
在Spring框架的日常开发中,Bean循环依赖是一个让不少开发者头疼的问题——明明代码逻辑看似合理,启动时却突然抛出BeanCurrentlyInCreationException,导致容器初始化失败。本文将从Spring Bean的创建流程出发,拆解循环依赖的本质,分析场景差异,并给出针对性的解决方案。
🔍 一、什么是Spring Bean循环依赖?
循环依赖指的是两个或多个Bean之间互相持有对方的引用,形成一个闭环的依赖关系。比如:
- A类的实例化依赖B类的实例
- B类的实例化又依赖A类的实例
// A类依赖B类
@Component
public class A {
private B b;
public A(B b) { this.b = b; }
}
// B类依赖A类
@Component
public class B {
private A a;
public B(A a) { this.a = a; }
}
当Spring容器尝试初始化这两个Bean时,就会陷入“先有鸡还是先有蛋”的死循环,最终导致初始化失败。
🧰 二、Spring默认支持的循环依赖场景
Spring并非完全无法处理循环依赖,它在特定场景下提供了内置的解决方案,核心依赖于三级缓存机制:
- 一级缓存(singletonObjects):存储完全初始化完成的单例Bean
- 二级缓存(earlySingletonObjects):存储提前暴露的、未完全初始化的Bean实例
- 三级缓存(singletonFactories):存储Bean的工厂对象,用于创建提前暴露的实例
支持的场景
- 单例Bean的 setter注入 / 字段注入:Spring通过提前暴露未完全初始化的Bean实例,结合三级缓存可以解决这类循环依赖
- 单例Bean的@Autowired字段注入:本质和setter注入类似,属于属性注入范畴
不支持的场景
- 构造方法注入的循环依赖:Spring在实例化Bean时必须先获取构造方法的参数,此时Bean尚未创建,无法提前暴露
- 原型Bean的循环依赖:原型Bean每次获取都会创建新实例,Spring不会为其提供缓存支持
⚠️ 三、循环依赖导致容器初始化失败的典型场景
场景1:构造方法注入循环依赖
这是最常见的失败场景,当两个单例Bean通过构造方法互相依赖时,Spring容器无法完成实例化:
@Component
public class ConstructorA {
private ConstructorB b;
// 构造方法注入B
public ConstructorA(ConstructorB b) { this.b = b; }
}
@Component
public class ConstructorB {
private ConstructorA a;
// 构造方法注入A
public ConstructorB(ConstructorA a) { this.a = a; }
}
启动时会直接抛出异常: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'constructorA': Requested bean is currently in creation: Is there an unresolvable circular reference?
场景2:原型Bean的循环依赖
无论使用哪种注入方式,原型Bean的循环依赖都会导致失败,因为Spring不会缓存原型Bean的实例:
@Scope("prototype")
@Component
public class PrototypeA {
@Autowired
private PrototypeB b;
}
@Scope("prototype")
@Component
public class PrototypeB {
@Autowired
private PrototypeA a;
}
每次获取PrototypeA时,都会创建新的PrototypeA和PrototypeB,最终陷入无限循环。
🛠️ 四、解决方案:针对不同场景的破局之道
方案1:修改注入方式(构造方法→setter/字段注入)
将构造方法注入改为setter注入或@Autowired字段注入,让Spring可以通过三级缓存解决循环依赖:
@Component
public class A {
private B b;
// 改为setter注入
@Autowired
public void setB(B b) { this.b = b; }
}
@Component
public class B {
private A a;
// 改为字段注入
@Autowired
private A a;
}
方案2:使用@Lazy延迟加载
在构造方法注入的参数上添加@Lazy注解,让Spring创建一个代理对象注入,延迟实际Bean的初始化:
@Component
public class ConstructorA {
private ConstructorB b;
// 延迟加载B的实例
public ConstructorA(@Lazy ConstructorB b) { this.b = b; }
}注意:这种方式下,第一次调用B的方法时才会真正初始化B实例,需要考虑线程安全问题。
方案3:使用@DependsOn指定依赖顺序
通过@DependsOn强制指定Bean的创建顺序,适用于非直接循环依赖的场景:
@Component
@DependsOn("B")
public class A {
private B b;
public A() {}
@Autowired
public void setB(B b) { this.b = b; }
}但该注解无法解决直接的双向循环依赖,仅能处理间接依赖的顺序问题。
方案4:手动注册Bean并打破循环
通过@Bean注解手动注册Bean,在注册过程中手动注入依赖,打破循环:
@Configuration
public class BeanConfig {
@Bean
public A a() {
A a = new A();
// 先创建A,再手动注入B
a.setB(b());
return a;
}
@Bean
public B b() {
B b = new B();
// 这里注入的是已经创建好的A实例
b.setA(a());
return b;
}
}
这种方式需要开发者手动控制Bean的创建流程,适合复杂场景下的精确控制。
方案5:重构代码(从根源避免循环依赖)
最彻底的解决方案是重构代码,通过引入中间层或拆分Bean的职责,打破循环依赖的闭环:
- 提取公共逻辑到独立的C类,让A和B都依赖C,而不是互相依赖
- 将双向依赖改为单向依赖,调整业务逻辑的职责划分
📝 五、总结与最佳实践
- 优先使用字段注入或setter注入:利用Spring内置的三级缓存机制,避免构造方法注入带来的循环依赖问题
- 构造方法注入仅用于强制依赖:如果使用构造方法注入,确保依赖关系是单向的,避免循环
- 避免滥用原型Bean:原型Bean的循环依赖无法通过Spring内置机制解决,尽量使用单例Bean
- 代码重构是最优解:循环依赖往往暗示着代码职责划分不清晰,重构代码从根源解决问题比寻找技术方案更有价值
Spring的循环依赖问题,本质是Bean创建流程和依赖关系的冲突。理解Spring的Bean创建机制,结合业务场景选择合适的解决方案,才能在开发中避免踩坑,保证容器的稳定初始化。