Spring循环依赖问题详解

79 阅读9分钟

Spring循环依赖问题详解

前言

在Spring框架的学习和使用中,循环依赖是一个比较常见的问题,尤其对于初学者来说,理解起来可能会有些困难。本文将深入浅出地讲解Spring中的循环依赖问题,包括其现象、原因、解决方案以及面试中可能涉及的相关问题,帮助大家更好地理解和掌握这一知识点。

一、循环依赖现象

1. 构造器循环依赖

当两个或多个Bean通过构造器互相依赖时,会导致项目启动失败。例如:

@Service
public class A {
    public A(B b) {  }
}

@Service
public class B {
    public B(C c) {
    }
}

@Service
public class C {
    public C(A a) {  }
}

在这种情况下,Spring无法在构造阶段解决这种循环依赖,因此会抛出异常。

2. field属性注入循环依赖(单例)

在单例情况下,Spring能够通过其内部机制解决循环依赖问题,项目可以正常启动。例如:

@Service
public class A1 {
    @Autowired
    private B1 b1;
}

@Service
public class B1 {
    @Autowired
    public C1 c1;
}

@Service
public class C1 {
    @Autowired  public A1 a1;
}

这里,尽管存在循环依赖,但由于是单例Bean,Spring能够成功初始化。

3. field属性注入循环依赖(prototype)

对于prototype作用域的Bean,循环依赖会导致初始化失败。例如:

@Service
@Scope("prototype")
public class A1 {
    @Autowired
    private B1 b1;
}

@Service
@Scope("prototype")
public class B1 {
    @Autowired
    public C1 c1;
}

@Service
@Scope("prototype")
public class C1 {
    @Autowired  public A1 a1;
}

在这种情况下,由于每次请求都会创建新的Bean实例,无法形成稳定的引用关系,因此会抛出异常。

二、循环依赖原因分析

1. SpringBean的加载流程

Spring在加载Bean时,会经历一系列步骤,包括配置文件的解析、BeanDefinition的注册、Bean的创建等。在创建Bean的过程中,Spring会根据依赖关系进行注入,而循环依赖问题正是在这一过程中产生的。

2. 单例与prototype的区别

单例Bean在整个应用中只有一个实例,Spring可以通过缓存机制来解决其循环依赖问题。而prototype Bean每次请求都会创建新的实例,没有稳定的引用关系,因此无法解决循环依赖。

三、Spring解决循环依赖的机制

1. 单例Bean的三级缓存

Spring通过singletonObjects(一级缓存)、earlySingletonObjects(二级缓存)和singletonFactories(三级缓存)这三个Map来管理单例Bean的创建过程。在Bean初始化的不同阶段,Spring会将Bean放入相应的缓存中,以确保在依赖注入时能够正确获取到Bean实例,从而解决循环依赖问题。

2. Bean的创建流程

Spring在创建Bean时,会先进行实例化,然后进行属性填充(依赖注入)。对于单例Bean,实例化后的对象会先放入三级缓存中,在依赖注入时从缓存中获取,避免了循环依赖导致的死循环。

四、面试回答思路和答案

问题1:什么是Spring中的循环依赖?有哪些类型?

回答思路:

  • 先解释循环依赖的基本概念,即两个或多个Bean互相依赖形成闭环。
  • 然后说明Spring中循环依赖的几种类型,包括构造器循环依赖、field属性注入循环依赖(单例和prototype)。

示例答案:
循环依赖是指在Spring中,两个或多个Bean互相持有对方的引用,形成闭环。例如,A依赖于B,B又依赖于A。Spring中的循环依赖主要有以下几种类型:

  1. 构造器循环依赖:通过构造器注入的方式形成的循环依赖。
  2. field属性注入循环依赖(单例):在单例Bean中通过field属性注入形成的循环依赖。
  3. field属性注入循环依赖(prototype):在prototype作用域的Bean中通过field属性注入形成的循环依赖。

问题2:Spring是如何解决循环依赖问题的?

回答思路:

  • 先提到Spring通过三级缓存机制来解决单例Bean的循环依赖问题。
  • 然后简要说明三级缓存的作用和工作原理。
  • 最后指出prototype作用域的Bean无法解决循环依赖问题的原因。

示例答案:
Spring通过三级缓存机制来解决单例Bean的循环依赖问题。具体来说,Spring使用singletonObjects(一级缓存)、earlySingletonObjects(二级缓存)和singletonFactories(三级缓存)这三个Map来管理单例Bean的创建过程。在Bean初始化的不同阶段,Spring会将Bean放入相应的缓存中,以确保在依赖注入时能够正确获取到Bean实例,从而解决循环依赖问题。对于prototype作用域的Bean,由于每次请求都会创建新的实例,没有稳定的引用关系,因此无法解决循环依赖问题。

问题3:在实际开发中,如何避免循环依赖问题?

回答思路:

  • 提到设计时应尽量避免出现循环依赖的情况。
  • 说明可以通过合理的模块划分和依赖关系调整来消除循环引用。
  • 提到在无法避免的情况下,可以选择合适的依赖注入方式,如优先使用field属性注入(单例)。

示例答案:
在实际开发中,应尽量避免出现循环依赖的情况。可以通过合理的模块划分和依赖关系调整来消除循环引用。例如,将相互依赖的类拆分到不同的模块中,或者通过引入中间类来打破循环依赖关系。如果无法避免循环依赖,可以选择合适的依赖注入方式,如优先使用field属性注入(单例),因为Spring能够较好地解决单例Bean的循环依赖问题。

五、总结

回到循环依赖的问题,Spring通过其精妙的三级缓存机制解决了单例Bean的循环依赖问题,但在并发场景下,每一步的执行都可能调用getBean方法,Spring通过缓存和对象锁确保了单例Bean的唯一性和正确性。解决循环依赖问题的关键在于理解Spring IOC和DI的整个流程,包括Bean的加载、创建、初始化和销毁等步骤。在实际开发中,应尽量避免循环依赖,通过合理的设计和依赖注入方式来解决问题。

希望本文能够帮助大家对Spring的IOC和DI的流程有一个更深刻的认识,为今后的学习和工作打下坚实的基础。

详细过程

Spring的三级缓存机制是其解决单例Bean循环依赖问题的关键。下面将详细讲解其具体工作原理:

1. 三级缓存的构成

Spring的三级缓存由以下三个Map构成:

  • 一级缓存(singletonObjects) :存储已经完全初始化好的单例Bean实例。当需要获取一个单例Bean时,Spring会优先从这个缓存中查找。如果找到,则直接返回,避免重复创建。
  • 二级缓存(earlySingletonObjects) :存储已经实例化但尚未完成依赖注入和初始化的Bean。这些Bean虽然还未完全准备好,但已经可以被其他Bean引用。通过这个缓存,其他Bean在依赖该Bean时可以获取到一个早期的实例。
  • 三级缓存(singletonFactories) :存储的是Bean的工厂对象。当Bean实例化完成,但还未完成属性注入和初始化时,会将一个创建该Bean代理对象的工厂存入此缓存。这个工厂对象可以在需要时返回Bean的早期实例。

2. 工作原理

当Spring容器检测到循环依赖时,它会通过以下步骤利用三级缓存来解决循环依赖问题:

步骤1:创建第一个Bean(如BeanA)
  • Spring容器开始创建BeanA,首先将BeanA的创建标记为正在创建状态。
  • 实例化BeanA,此时BeanA只是一个空的实例,还未进行属性注入和初始化。
  • 将创建BeanA的工厂对象存入三级缓存singletonFactories中。
步骤2:注入依赖的第二个Bean(如BeanB)
  • 在对BeanA进行属性注入时,发现BeanA依赖BeanB,于是Spring容器开始创建BeanB。
步骤3:创建第二个Bean(BeanB)
  • 同样,将BeanB的创建标记为正在创建状态。
  • 实例化BeanB,然后将创建BeanB的工厂对象存入三级缓存singletonFactories中。
步骤4:BeanB注入依赖的BeanA
  • 在对BeanB进行属性注入时,发现BeanB依赖BeanA。此时,Spring会先从一级缓存singletonObjects中查找BeanA,发现没有找到。
  • 接着从三级缓存singletonFactories中查找BeanA的工厂对象,找到后通过该工厂对象获取BeanA的早期实例,并将这个早期实例存入二级缓存earlySingletonObjects中,同时从三级缓存中移除该工厂对象。
  • 将这个早期的BeanA实例注入到BeanB中。
步骤5:完成BeanB的创建
  • BeanB完成属性注入和初始化后,将BeanB存入一级缓存singletonObjects中,并从正在创建状态标记中移除。
步骤6:完成BeanA的创建
  • 此时BeanA可以从一级缓存中获取到已经完全初始化好的BeanB实例,完成属性注入和初始化。
  • 最后将BeanA存入一级缓存singletonObjects中,并从二级缓存earlySingletonObjects中移除。

3. 为什么需要三级缓存

  • 避免重复创建:二级缓存中的工厂对象每次调用都会生成一个新的早期实例。如果多个Bean依赖同一个未初始化的Bean,直接通过工厂生成实例会导致多次创建,造成性能浪费。三级缓存通过存储已生成的早期实例,避免了重复创建。
  • 支持AOP代理:在Spring中,若Bean需要被代理(如通过@Transactional@Async),代理对象的创建必须在属性注入前完成。三级缓存存储的是工厂生成的代理对象,这样其他Bean注入的是代理而非原始实例,确保代理逻辑的正确性。

4. 总结

Spring的三级缓存机制通过在Bean创建的不同阶段提供不同的缓存策略,有效地解决了单例Bean的循环依赖问题。一级缓存用于存储完全初始化的Bean,二级缓存用于存储早期暴露的Bean实例,三级缓存用于存储Bean的工厂对象。这种机制不仅避免了循环依赖导致的死锁问题,还优化了Bean的创建和初始化过程,确保了Spring容器的高效运行。