
1. 循环依赖的产生原因
Java Spring 中循环依赖的原因一般是因为两个或多个 bean 相互依赖,形成了一个循环依赖的环路。通俗地讲,A 依赖 B,B 又依赖 A。
Spring 依赖注入机制的目的是将启动时需要创建的对象(通常称为 bean)及它们之间的依赖关系全部交付给 Spring 容器进行管理和维护,通过配置(如 XML 或注解等方式)告诉 Spring 需要创建哪些对象,每个对象需要依赖哪些其它对象,Spring 运行时会自动去解决对象之间的依赖关系。
循环依赖问题是在 Spring 运行时进行对象依赖注入时引起的,如果在对象依赖注入过程中,出现了 A->B->C->A 这样的环形依赖关系,就会导致循环依赖问题的出现。
具体来讲,Spring 在进行对象注入时有两种策略:构造函数注入和 Setter 方法注入。
构造函数注入:
- 容器创建 Bean A 实例。
- 创建 Bean A 时,如果发现需要容器创建 Bean B 的实例,则会暂时将 A 对象存储到一个缓存区中,然后记录下所需注入属性的类型和 Bean 名称。
- 容器创建 Bean B 实例。
- 创建 Bean B 实例时,如果发现需要容器创建 Bean A 的实例,则会暂时将 B 对象存储到一个缓存区中,然后记录下所需注入属性的类型和 Bean 名称。
- 容器会尝试按照缓存区中的属性类型和名称获取对应的 Bean 进行注入,这个时候会从缓存区中获取相应的 bean,如果没有则将 A 对象加入到创建 B 对象时记录的 A 需要注入的属性的列表中去,返回到容器去继续初始化其他的 Bean,直到将所有的 Bean 初始化完成,再在之前缓存的 Bean 中进行对应的属性注入。
Setter 方法注入:
- 容器创建 A 实例。
- 容器注入 A 实例所需的属性,如果发现需要创建 B 实例,则会临时将 A 对象存储起来,并记录需要注入属性的类型和 Bean 名称。
- 容器创建 B 实例。
- 容器注入 B 实例所需的属性,如果发现需要创建 A 实例,则会临时将 B 对象存储起来,并记录需要注入属性的类型和 Bean 名称。
- 容器会尝试按照临时存储的对象类型和名称获取对应的 Bean 进行注入,这个时候会从临时存储的对象中获取相应的 Bean,如果没有则将 A 对象加入到创建 B 对象时记录的 A 需要注入的属性的列表中去(注意和构造函数注入的区别),返回到容器去继续初始化其他 Bean,知道将 BEan 的所有属性注入完成。
需要注意的是,循环依赖并不是所有情况都会出现的,像单例模式下的循环依赖就不会出现,这是因为 Spring 容器缓存的是对象的单例实例,而不是对象本身。
- 循环依赖的解决方案
解决循环依赖问题,可以使用构造函数注入或者使用@Lazy注解进行懒加载。
2.1 使用构造函数注入
使用构造函数注入,正是因为构造函数的注入是一次性的,如果需要解决循环依赖,循环依赖的双方需要在构造函数中传递一个代理对象。
假设有A和B依赖于彼此,结构代码如下:
public class A {
private B b;
public A(B b) {
this.b = b;
}
// getters and setters
}
public class B {
private A a;
public B(A a) {
this.a = a;
}
// getters and setters
}
我们可以使用 Spring 的构造函数注入来解决循环依赖。
<bean id="a" class="com.example.A">
<constructor-arg ref="b"/>
</bean>
<bean id="b" class="com.example.B">
<constructor-arg ref="a"/>
</bean>
这样,Spring 在创建 A 的时候会先创建 B,将 B 传入到 A 的构造函数中,再将 A 传入到 B 的构造函数中,这样就成功解决了循环依赖的问题。
2.2 使用 @Lazy 注解进行懒加载
使用 @Lazy 注解进行懒加载是另一种解决循环依赖的方案。在 Spring 5.x 版本中,新增了一个特性,即默认情况下支持循环依赖,它会创建代理对象,并将代理对象暂时注册到 BeanFactory 中。
示例代码如下:
@Lazy
@Component
public class A {
@Autowired
private B b;
// getters and setters
}
@Lazy
@Component
public class B {
@Autowired
private A a;
// getters and setters
}
这样,当创建 A 时,容器中就先存在了一个 A 的代理对象,而真正的 A 对象会在 B 创建完成后再初始化创建,因为此时 B 已经存在了一个代理对象,再把这个代理对象注入到 A 中,就成功解决了循环依赖的问题。
- 循环依赖的注意事项
3.1 构造注入不能解决循环依赖的问题
尽管通过构造函数注入很容易解决循环依赖的问题,但是构造注入又陷入另一个问题——它无法解决循环依赖的问题,例如 A 依赖 B,B 依赖 A,在进行构造注入的时候,即便在 A 和 B 中使用构造注入,它也无法在创建两个对象之后自动解决循环依赖。
3.2 按需注入可以解决循环依赖
按需注入指的是 Spring 在需要使用 bean 时才进行注入,而不是在创建时就注入。如果两个 bean 都使用按需注入,那么 Spring 可以在第一次访问 bean 的时候解决循环依赖的问题。
3.3 需要注意的是循环依赖不仅限于同一个 ApplicationContext
如果两个 bean 分别被不同的 ApplicationContext 创建,如果两个 ApplicationContext 之间产生了循环依赖的关系,Spring 也无法直接解决这个问题,需要手动合并这两个 ApplicationContext 或使用其他方法解决。
文章评论