写点什么

Java 面试题:Spring 中的循环依赖,给程序员带来的心理阴影

  • 2024-05-17
    福建
  • 本文字数:2082 字

    阅读完需:约 7 分钟

循环依赖通常发生在两个或多个 Spring Bean 之间,它们通过构造器、字段(使用 @Autowired)或 setter 方法相互依赖,从而形成一个闭环。下面是一个使用字段注入(即使用 @Autowired)导致的循环依赖的示例:

 

示例代码: 


假设我们有两个类,ClassA 和 ClassB,它们相互依赖:

public class ClassA {      @Autowired     private ClassB classB;      // ... 其他代码 ... }  @Component public class ClassB {      @Autowired     private ClassA classA;      // ... 其他代码 ... }
复制代码


在上面的示例中,ClassA 依赖 ClassB,而 ClassB 又依赖 ClassA。当 Spring 容器启动时,它会尝试为这两个类创建 bean 的实例。但是,由于它们之间的循环依赖,这会导致问题。



问题说明: 


当 Spring 容器创建 ClassA 的 bean 时,它会发现 ClassA 依赖于 ClassB,所以它会尝试创建 ClassB 的 bean。当 Spring 容器创建 ClassB 的 bean 时,它又会发现 ClassB 依赖于 ClassA,但此时 ClassA 的 bean 还没有被完全初始化(因为它正在等待 ClassB 的 bean),这就形成了一个死循环。


那么 Spring 是如何解决这种循环依赖问题的?


三级缓存机制:


Spring 容器在创建 bean 的过程中,会维护三个缓存,分别是:singletonObjects(一级缓存)、earlySingletonObjects(二级缓存)和 singletonFactories(三级缓存)。


当 Spring 容器开始实例化一个 bean 时,会先将其 ObjectFactory(对象工厂)放入三级缓存中。如果在实例化过程中需要注入其他 bean,并且这个 bean 也正在实例化中,Spring 会先从一级缓存中查找该 bean 的实例。如果没有找到,会到二级缓存中查找。如果二级缓存中也没有,那么会到三级缓存中查找 ObjectFactory,并调用 ObjectFactory 的 getObject 方法来获取 bean 的实例。


当获取到 bean 的实例后,会将其放入二级缓存中,并从三级缓存中移除 ObjectFactory。


当 bean 的实例化过程完成后,会将其放入一级缓存中。


通过这种方式,Spring 可以在 bean 的实例化过程中解决循环依赖问题。


@Lazy 注解:


在 Spring 中,可以使用 @Lazy 注解来延迟 bean 的初始化。当一个 bean 被标记为 @Lazy 时,Spring 容器在启动时不会立即实例化它,而是在第一次被使用时才进行实例化。


通过将循环依赖的 bean 声明为懒加载,可以延迟它们的初始化过程,从而避免在容器启动时发生循环依赖问题。


需要注意的是,@Lazy 注解只能用于单例作用域的 bean,并且要求依赖项必须是接口类型。

 

以下是使用 @Lazy 注解来解决循环依赖的示例代码:

@Component public class ClassA {      private final ClassB classB;      // 使用@Autowired和@Lazy注解来延迟ClassB的注入     @Autowired     public ClassA(@Lazy ClassB classB) {         this.classB = classB;     }      // ... 其他代码 ... }  @Component public class ClassB {      private final ClassA classA;      // 使用@Autowired和@Lazy注解来延迟ClassA的注入     @Autowired     public ClassB(@Lazy ClassA classA) {         this.classA = classA;     }      // ... 其他代码 ... }
复制代码


在上面的示例中,@Lazy 注解被用于 ClassA 和 ClassB 的构造器参数上,以延迟它们之间的依赖注入。这意味着在创建 ClassA 时,它不会立即尝试去初始化 ClassB,而是会得到一个代理对象。同样,在创建 ClassB 时,它也不会立即初始化 ClassA。

 

避免构造器循环依赖:


在 Spring 中,构造器循环依赖是无法解决的,因为构造器在实例化 bean 的过程中被调用,如果两个 bean 相互依赖对方的构造器,那么就会形成死锁。


因此,在设计 bean 的依赖关系时,应该尽量避免使用构造器注入来创建循环依赖。可以使用 setter 注入或字段注入来代替构造器注入。

 

下面是一个构造器循环依赖的错误代码示例:

@Component public class ClassA {      private final ClassB classB;      // 通过构造器注入ClassB,形成循环依赖     @Autowired     public ClassA(ClassB classB) {         this.classB = classB;     }      // ... 其他代码 ... }  @Component public class ClassB {      private final ClassA classA;      // 通过构造器注入ClassA,与ClassA形成循环依赖     @Autowired     public ClassB(ClassA classA) {         this.classA = classA;     }      // ... 其他代码 ... }
复制代码


在上面的示例中,ClassA 和 ClassB 各自在构造函数中依赖于对方,这就形成了构造器循环依赖。

 

当 Spring 容器尝试创建这两个 bean 的实例时,会遇到问题: 

  • Spring 首先尝试创建 ClassA 的实例,并发现它需要 ClassB 的实例。 

  • 然后,Spring 尝试创建 ClassB 的实例,但发现它需要 ClassA 的实例。 

  • 由于 ClassA 的实例正在等待 ClassB 的实例,而 ClassB 的实例又正在等待 ClassA 的实例,这导致了一个死循环,无法继续创建 bean 的实例。

 

综上所述,Spring 通过三级缓存机制、@Lazy 注解以及避免构造器循环依赖等方式来解决循环依赖问题。这些机制使得 Spring 容器能够更加灵活地处理 bean 之间的依赖关系,提高系统的可维护性和可扩展性。


文章转载自:猫鱼吐泡泡

原文链接:https://www.cnblogs.com/marsitman/p/18195525

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Java面试题:Spring中的循环依赖,给程序员带来的心理阴影_Java_快乐非自愿限量之名_InfoQ写作社区