依赖
约 813 字大约 3 分钟
2025-07-24
请举一个 Spring 发生循环依赖的例子,要求是三级缓存无法解决的场景。
好的。Spring 的三级缓存机制确实能够解决绝大多数 singleton
作用域下,基于字段注入 (@Autowired
field) 或 Setter 注入的循环依赖问题。但是,有一种场景是它无法解决的,那就是基于构造器的循环依赖。
当两个或多个 Bean 通过构造器相互注入时,会形成一个无法被 Spring 容器解决的闭环,最终导致 BeanCurrentlyInCreationException
异常。
场景构造
假设我们有两个服务类,ServiceA
和 ServiceB
。ServiceA
的构造器需要一个 ServiceB
的实例,同时 ServiceB
的构造器也需要一个 ServiceA
的实例。
ServiceA 的代码:
Java
import org.springframework.stereotype.Service;
@Service
public class ServiceA {
private final ServiceB serviceB;
// 构造器注入 ServiceB
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
System.out.println("ServiceA instantiated.");
}
public void doSomething() {
System.out.println("ServiceA is doing something.");
}
}
ServiceB 的代码:
Java
import org.springframework.stereotype.Service;
@Service
public class ServiceB {
private final ServiceA serviceA;
// 构造器注入 ServiceA
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
System.out.println("ServiceB instantiated.");
}
public void doSomethingElse() {
System.out.println("ServiceB is doing something else.");
}
}
为什么三级缓存无法解决?
我们来分析一下 Spring 容器在尝试创建这两个 Bean 时的过程:
创建
ServiceA
:Spring 容器尝试创建
ServiceA
的实例。它发现ServiceA
有一个需要ServiceB
的构造器。此时
ServiceA
正在创建中,Spring 会将ServiceA
的 beanName(例如"serviceA"
)标记为“正在创建”。为了解决依赖,Spring 容器会去查找
ServiceB
的 Bean。
创建
ServiceB
:Spring 容器开始创建
ServiceB
的实例。它发现ServiceB
的构造器需要一个ServiceA
的实例。容器会去查找
ServiceA
的 Bean。
发现循环依赖:
- 在查找
ServiceA
的过程中,Spring 发现ServiceA
已经被标记为“正在创建”。这就形成了一个闭环:A -> B -> A
。
- 在查找
关键问题点:
Spring 的三级缓存 (singletonFactories
) 能够解决循环依赖,是因为它可以提前暴露一个半成品对象(通过 ObjectFactory)。对于字段注入,这个半成品对象(一个尚未完成属性填充的代理对象)可以被提前注入到依赖它的对象中,从而打破循环。
但是,对于构造器注入,情况完全不同:
对象的实例化是通过调用构造器完成的。
要调用
new ServiceA(serviceB)
,就必须先拥有一个完整的、已经实例化好的serviceB
对象。同理,要调用
new ServiceB(serviceA)
,也必须先拥有一个完整的、已经实例化好的serviceA
对象。
在这个场景下,不存在“半成品”对象的概念。在调用构造器之前,对象实例本身还不存在,自然也无法将其放入三级缓存中提前暴露。两个 Bean 的创建过程互相等待对方的实例化完成,形成了一个死锁,Spring 容器无法打破这个循环,只能抛出异常。
因此,这种通过构造器形成的循环依赖,是 Spring 设计上就无法解决的,需要开发者在设计代码结构时主动避免。修复方式通常是修改其中一个类的注入方式,例如将构造器注入改为 Setter 注入或字段注入。