Spring Bean循环依赖导致的容器初始化失败

VIP/

在Spring框架的日常开发中,Bean循环依赖是一个让不少开发者头疼的问题——明明代码逻辑看似合理,启动时却突然抛出BeanCurrentlyInCreationException,导致容器初始化失败。本文将从Spring Bean的创建流程出发,拆解循环依赖的本质,分析场景差异,并给出针对性的解决方案。


🔍 一、什么是Spring Bean循环依赖?

循环依赖指的是两个或多个Bean之间互相持有对方的引用,形成一个闭环的依赖关系。比如:

  • A类的实例化依赖B类的实例
  • B类的实例化又依赖A类的实例
Java
复制
// 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并非完全无法处理循环依赖,它在特定场景下提供了内置的解决方案,核心依赖于三级缓存机制:

  1. 一级缓存(singletonObjects):存储完全初始化完成的单例Bean
  2. 二级缓存(earlySingletonObjects):存储提前暴露的、未完全初始化的Bean实例
  3. 三级缓存(singletonFactories):存储Bean的工厂对象,用于创建提前暴露的实例

支持的场景

  • 单例Bean的 setter注入 / 字段注入:Spring通过提前暴露未完全初始化的Bean实例,结合三级缓存可以解决这类循环依赖
  • 单例Bean的@Autowired字段注入:本质和setter注入类似,属于属性注入范畴

不支持的场景

  • 构造方法注入的循环依赖:Spring在实例化Bean时必须先获取构造方法的参数,此时Bean尚未创建,无法提前暴露
  • 原型Bean的循环依赖:原型Bean每次获取都会创建新实例,Spring不会为其提供缓存支持

⚠️ 三、循环依赖导致容器初始化失败的典型场景

场景1:构造方法注入循环依赖

这是最常见的失败场景,当两个单例Bean通过构造方法互相依赖时,Spring容器无法完成实例化:

Java
复制
@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的实例:

Java
复制
@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可以通过三级缓存解决循环依赖:

Java
复制
@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的初始化:

Java
复制
@Component
public class ConstructorA {
private ConstructorB b;
// 延迟加载B的实例
public ConstructorA(@Lazy ConstructorB b) { this.b = b; }
}

注意:这种方式下,第一次调用B的方法时才会真正初始化B实例,需要考虑线程安全问题。

方案3:使用@DependsOn指定依赖顺序

通过@DependsOn强制指定Bean的创建顺序,适用于非直接循环依赖的场景:

Java
复制
@Component
@DependsOn("B")
public class A {
private B b;
public A() {}
@Autowired
public void setB(B b) { this.b = b; }
}

但该注解无法解决直接的双向循环依赖,仅能处理间接依赖的顺序问题。

方案4:手动注册Bean并打破循环

通过@Bean注解手动注册Bean,在注册过程中手动注入依赖,打破循环:

Java
复制
@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,而不是互相依赖
  • 将双向依赖改为单向依赖,调整业务逻辑的职责划分

📝 五、总结与最佳实践

  1. 优先使用字段注入或setter注入:利用Spring内置的三级缓存机制,避免构造方法注入带来的循环依赖问题
  2. 构造方法注入仅用于强制依赖:如果使用构造方法注入,确保依赖关系是单向的,避免循环
  3. 避免滥用原型Bean:原型Bean的循环依赖无法通过Spring内置机制解决,尽量使用单例Bean
  4. 代码重构是最优解:循环依赖往往暗示着代码职责划分不清晰,重构代码从根源解决问题比寻找技术方案更有价值

Spring的循环依赖问题,本质是Bean创建流程和依赖关系的冲突。理解Spring的Bean创建机制,结合业务场景选择合适的解决方案,才能在开发中避免踩坑,保证容器的稳定初始化。

购买须知/免责声明
1.本文部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责。
2.若您需要商业运营或用于其他商业活动,请您购买正版授权并合法使用。
3.如果本站有侵犯、不妥之处的资源,请在网站右边客服联系我们。将会第一时间解决!
4.本站所有内容均由互联网收集整理、网友上传,仅供大家参考、学习,不存在任何商业目的与商业用途。
5.本站提供的所有资源仅供参考学习使用,版权归原著所有,禁止下载本站资源参与商业和非法行为,请在24小时之内自行删除!
6.不保证任何源码框架的完整性。
7.侵权联系邮箱:aliyun6168@gail.com / aliyun666888@gail.com
8.若您最终确认购买,则视为您100%认同并接受以上所述全部内容。

免费源码网 java Spring Bean循环依赖导致的容器初始化失败 https://svipm.com.cn/21260.html

相关文章

猜你喜欢