Spring循环依赖问题详解

Spring循环依赖问题详解

1. Spring循环依赖问题?

1.1 什么是循环依赖

​在 Spring 中,循环依赖是指两个或多个 Bean 之间存在相互依赖的情况,即 Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A,形成了一个循环依赖链。这种情况会导致 Spring IoC 容器无法正确地初始化这些 Bean,从而引发循环依赖异常。Spring 在检测到潜在的循环依赖时会抛出异常,以防止无限递归。

以下是一个简单的代码示例来说明循环依赖问题:

kotlin 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class BeanA {
    private BeanB beanB;

    @Autowired
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}

@Component
public class BeanB {
    private BeanA beanA;

    @Autowired
    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }
}

在这个示例中,BeanA 依赖于 BeanB,而 BeanB 依赖于 BeanA,形成了循环依赖。当 Spring 容器尝试初始化这两个 Bean 时,它会检测到循环依赖,从而引发异常。

如果不考虑Spring框架,那么循环依赖其实并不是个问题,如下:

ini 复制代码
BeanA A = new BeanA();
BeanB B = new BeanB();
A.beanB = B;
B.beanA = A;

但是Spring中的Bean是经历一系列Bean的生命周期创建出来的,在Spring中的Bean的创建过程中,如果出现循环依赖就可能会导致无限递归地去创建Bean,因此,当Spring框架检测到循环依赖的时候就会先抛出循环依赖的异常。但是,如果想要更加清楚的理解循环依赖的过程以及循环依赖所会造成的问题,这里我们需要先了解Spring Bean的生命周期,因此在1.2中我们先详细了解什么是Bean的生命周期。

1.2 Spring Bean的生命周期

如上图所示,​Spring Bean的生命周期可以分为以下阶段,包括Aware接口的调用和BeanPostProcessor的应用:

  1. 加载Bean定义信息:Spring扫描xm、properties、yml和json等定义的Bean信息,得到Bean Definition。

  2. 实例化(Instantiation):容器根据BeanDefinition去创建Bean的实例。这涉及将类加载到内存中并创建Bean对象。在这个阶段,首先会根据class推断出构造方法,根据推断出的构造方法反射得到一个对象(原始对象)。

  3. 属性注入/依赖注入(Population):在Bean实例化后,容器会将Bean的属性值注入到Bean中。这可以通过构造函数注入、Setter方法注入或字段注入来实现。

  4. Aware接口:当IOC容器创建的bean对象在进行具体操作的时候,如果需要容器的其它对象,此时可以将对象实现Aware接口,来满足当前的需要。

  5. BeanPostProcessor的前置处理(Pre-Initialization):如果已注册BeanPostProcessor,它们会在Bean的初始化之前调用,允许执行自定义的前置处理逻辑。如果原始对象有AOP切面,则需要根据原始对象生成一个代理对象。

  6. Bean初始化方法(Initialization):如果Bean配置了初始化方法,容器会在属性注入后调用该方法。这允许Bean执行任何初始化操作。同时,BeanPostProcessor的postProcessBeforeInitialization方法也会在此时被调用。

  7. BeanPostProcessor的后置处理(Post-Initialization):在Bean的初始化方法执行之后,BeanPostProcessor的postProcessAfterInitialization方法会被调用,允许执行自定义的后置处理逻辑。

  8. Bean可用(Bean is ready):此时,Bean可以在应用程序中使用,它已经完成了初始化。最终生成的bean对象会被放入单例池(singletonObjects)中,下次getBean的时候直接从单例池中获取即可。

  9. Bean销毁方法(Destruction):如果配置了销毁方法,容器在关闭时会调用该方法,以执行资源清理和释放。与此同时,BeanPostProcessor的方法也会在销毁前后调用。

整个生命周期是由Spring容器管理的,确保Bean的正确创建、初始化、使用和清理。Aware接口用于向Bean提供额外的信息,而BeanPostProcessor用于自定义Bean初始化前后的处理逻辑。这些机制增加了Spring Bean生命周期的灵活性和可定制性。

1.3 Spring Bean初始化流程中循环依赖导致的问题

如1.2中所示,在Spring容器创建好初始化对象之后,需要通过依赖注入的方法为初始化Bean填充属性进行依赖注入,这个流程如下:

kotlin 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class BeanA {
    private BeanB beanB;

    @Autowired
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}

@Component
public class BeanB {
    private BeanA beanA;

    @Autowired
    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }
}
  • 类BeanA中依赖了一个属性BeanB,所以当BeanA进行依赖注入的时候就需要对beanB进行赋值;

  • Spring会根据BeanB的名字和类型去BeanFactory(ApplicationContext)中获取BeanB对应类型的bean;

    • 情况1:如果BeanFactory中存在BeanB类型的bean,那么直接赋值给beanB即可;

    • 情况2:如果BeanFactory中不存在BeanB类型的bean,则需要生成一个BeanB类型对应的bean,然后赋值给beanB;

Spring循环依赖的问题就出现在情况2,如果此时BeanB类在BeanFactory中还没有生成对应的Bean,那么就需要去生成BeanB的bean,此时会走一遍BeanB的生命周期。

但是在创建BeanB类型的过程中,由于BeanB类依赖了BeanA类型,而此时BeanA的初始化还没有完成,所以此时就出现了循环依赖问题,导致BeanA和BeanB都无法完成Bean的初始化流程,甚至导致无限递归调用。

2. 如何解决循环依赖?

在上面的例子中,讲述了什么是循环依赖以及循环依赖会导致些什么问题,那么如何避免循环依赖呢?

2.1 Spring的三级缓存机制

​Spring 中的三级缓存是一种机制,用于处理 Bean 的循环依赖。这三个级别的缓存分别是单例对象缓存、早期对象缓存和完全初始化的对象缓存。

  1. 单例对象缓存/一级缓存(singletonObjects):

    • 在 Spring 容器中,所有的单例 Bean(默认情况下)都会被缓存到第一级缓存中;

    • 一旦 Bean 完全初始化,包括属性注入、初始化方法执行等就会被放入一级缓存当中;

    • 一级缓存已完全初始化 Bean,即 Bean 的状态完全准备好,可以在应用程序中使用,直接使用context.getBean()就可以获取到Bean对象;

  2. 早期对象缓存/二级缓存(earlySingletonObjects):

    • 当容器实例化 Bean 时,如果发现该 Bean 有循环依赖,容器会将该 Bean 的早期引用存储在第二级缓存中。
    • 早期引用是一个尚未初始化完成的 Bean,只包含 Bean 的实例,但尚未注入属性。(没有注入完成的当前Bean或者是提前AOP的Bean)
  3. 完全初始化的对象缓存/三级缓存(singletonFactories):

    • ObjectFactory,表示对象工厂,用来创建某个对象的。保存的是一个Lamda表达式,表达式的程序执行结果就是要放入二级缓存的对象。

Spring 的三级缓存机制用于解决循环依赖问题。当容器遇到循环依赖时,它会从第一级缓存中查找 Bean 的定义,然后将 Bean 放入第二级缓存作为早期引用,以便在初始化后注入属性。一旦 Bean 完全初始化,它将移至第三级缓存,以供应用程序使用。这个机制确保了 Bean 的正确初始化,同时避免了死锁和启动失败,使 Spring 成为一个功能强大的依赖注入容器。

2.1.1 缓存为什么能解决循环依赖问题?

我们来回顾下循环依赖问题的产生: BeanA初始化->依赖BeanB->BeanB初始化->依赖BeanA

那么如何打破这个循环呢,如果我们在Bean的属性注入的过程中,加入一个缓存,是不是就能解决这个问题呢?

如上面的一级缓存简图所示:

  • BeanA在创建过程中,在依赖注入之前先将BeanA的原始对象放入缓存中(提早暴露),之后再进行依赖注入;

  • BeanA在依赖注入过程中发现依赖BeanB,则去创建BeanB对象;

  • BeanB对象,先创建一个原始BeanB对象,将BeanB对象提前暴露放入缓存中,然后再对BeanA对象进行依赖注入。此时由于BeanA对象的原始对象已经存在于缓存当中,BeanB就可以完成对beanA对象的依赖注入,完成创建流程;

但是上面的一级缓存方案中存在着一个问题:如果BeanA对象的原始对象注入给BeanB的属性之后,BeanA的原始对象在BeanPostProcessor过程中进行了AOP操作,产生了一个代理对象。此时,正确的beanA对象应该是AOP之后的代理对象,但是BeanB中注入的BeanA对象却是原始对象,这样就会出现问题------BeanB依赖的BeanA和最终的BeanA不是同一个对象。

造成这个问题的原因在于,SpringBean提供的BeanPostProcessor方法可以对Bean进行加工,这个加工不仅仅能修改Bena的属性值,也可以替换掉当前的Bean。并且BeanPostProcessor执行在依赖注入之后,所以很有可能会导致注入给BeanB对象的BeanA对象和经历完整生命周期后的BeanA对象不是同一个对象的问题。

所以在这种情况下的循环依赖,一级缓存是解决不了的,因为在属性注入时,Spring也不知道A对象后续会经过哪些BeanPostProcessor以及会对A对象做什么处理。 ​

2.1.2 三级缓存原理

如上图所示:

  • Bean初始化过程中,当完成原始对象的初始化之后,会提前将Bean工厂的Lambda表示式暴露到三级缓存(singletonFactories)中,三级缓存中存储的是Bean工厂的lambda表达式,该表达式执行完毕之后就可以得到完整的Bean对象;

  • BeanA在依赖注入过程中会先从一级缓存(singletonObjects)中尝试获取BeanB,一级缓存中都是些已经完全初始化成功的Bean,如果存在则直接获取完成依赖注入;

  • 如果一级缓存中获取BeanB失败,则会开始创建BeanB对象,创建BeanB对象的过程同样会遇到依赖BeanA的属性注入的情况。

  • 此时同样先尝试从一级缓存中获取完整的beanA对象,获取失败则从二级缓存(earlySingletonObjects)中获取;

  • 如果存在于二级缓存中,则执行lambda表达式,创建一个完整的Bean对象后,放入一级缓存,完成依赖注入;

  • 如果不存在于二级缓存中,则从三级缓存中获取提前暴露的Bean工厂对象,并放入二级缓存中,执行lambda表达式,放入一级缓存,完成依赖注入。

总结一下三级缓存解决循环依赖问题主要是为了解决BeanPostProcessor过程中对Bean属性进行修改造成的缓存中存储的Bean和最终完整的Bean不匹配的问题。

2.2 其它解决循环依赖的方法

2.2.1 构造函数注入

​在 Spring 中,使用构造函数注入来解决循环依赖问题的原理是在Bean的实例化过程中,Spring容器会根据构造函数参数来解析依赖关系,并确保依赖的Bean在创建时可用。下面是一个代码示例,演示如何使用构造函数注入来解决循环依赖问题:

kotlin 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class BeanA {
    private final BeanB beanB;

    @Autowired
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }

    // ...
}

@Component
public class BeanB {
    private final BeanA beanA;

    @Autowired
    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }

    // ...
}

在这个示例中,BeanA和BeanB之间存在循环依赖关系。Spring容器在创建BeanA时,发现它依赖BeanB,然后尝试创建BeanB。当创建BeanB时,它依赖BeanA,但由于BeanA的实例已经在构造函数注入阶段创建完成,因此可以成功注入BeanA。

构造函数注入的原理在于Spring容器在实例化Bean时会先创建一个Bean的原始对象,将依赖的Bean注入到原始对象中,然后再完成Bean的初始化。这保证了Bean的依赖在Bean完成初始化之前已经准备好,从而解决了循环依赖问题。

2.2.2 Setter​注入

​使用Setter方法注入来解决循环依赖是一种常见的方法,它允许循环依赖的Bean通过Setter方法在运行时动态注入依赖,从而避免初始化时的问题。以下是一个示例代码来演示如何使用Setter方法注入来解决循环依赖,以及解释原理:

示例代码:

typescript 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class BeanA {
    private BeanB beanB;

    @Autowired
    public void setBeanB(BeanB beanB) {
        this.beanB = beanB;
    }

    // Other methods and properties
}

@Component
public class BeanB {
    private BeanA beanA;

    @Autowired
    public void setBeanA(BeanA beanA) {
        this.beanA = beanA;
    }

    // Other methods and properties
}

原理:

  1. Spring容器开始实例化BeanA,发现它依赖BeanB,但不会立即注入依赖。相反,它实例化BeanA,并将其添加到容器中,但在初始化BeanA时不注入依赖。

  2. Spring容器继续实例化BeanB,发现它依赖BeanA。同样,它实例化BeanB,并将其添加到容器中,但在初始化BeanB时不注入依赖。

  3. 现在,容器中有BeanA和BeanB的实例,但它们的依赖尚未满足。

  4. Spring容器在依赖注入阶段,检测到BeanA和BeanB的Setter方法(setBeanB 和 setBeanA),并调用这些Setter方法来注入依赖。这时,BeanA的setBeanB 方法被调用,注入BeanB的实例,同时BeanB的setBeanA方法被调用,注入BeanA的实例。

  5. 现在,BeanA和BeanB的依赖关系得到满足,它们可以在完成初始化后被正常使用。

这种方法的关键在于延迟依赖注入,确保BeanA和BeanB都已实例化但尚未注入依赖。然后,Spring容器通过Setter方法来满足它们的依赖。这种方式解决了循环依赖问题,同时避免了不必要的复杂性。

潜在问题:

  • Setter方法可能会被多次调用,需要确保依赖注入的顺序是正确的。
  • Setter方法需要提供对外的可访问性,可能会暴露一些不必要的细节。
  • Setter方法的调用顺序需要谨慎处理,以确保正确的依赖注入顺序。
  • Setter方法引入了可选依赖,如果某些依赖是可选的,需要额外的处理。
  • Setter方法不适用于不可变对象,对于不可变对象,Setter方法注入可能不适用。

​2.2.3 @Lazy​注入

​ 使用@Lazy注解可以解决Spring中的循环依赖问题。@Lazy注解告诉Spring容器要延迟初始化Bean,以避免循环依赖的问题。下面是一个示例代码,演示如何使用@Lazy注解来解决循环依赖,以及解释原理:

示例代码:

kotlin 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Component
public class BeanA {
    private BeanB beanB;

    @Autowired
    public BeanA(@Lazy BeanB beanB) {
        this.beanB = beanB;
    }

    // Other methods and properties
}

@Component
public class BeanB {
    private BeanA beanA;

    @Autowired
    public BeanB(@Lazy BeanA beanA) {
        this.beanA = beanA;
    }

    // Other methods and properties
}

原理:

  1. Spring容器开始实例化BeanA,发现它依赖BeanB,并使用@Lazy注解告诉容器要延迟初始化BeanB。

  2. Spring容器继续实例化BeanB,发现它依赖BeanA,并使用@Lazy注解告诉容器要延迟初始化BeanA。

  3. 现在,容器中有BeanA和BeanB的实例,但它们的依赖尚未满足。

  4. Spring容器完成BeanA和BeanB的实例化,但它们的依赖并没有被立即注入,因为BeanB和BeanA都被标记为@Lazy。

  5. 当需要使用BeanA或BeanB时,Spring容器会延迟初始化它们,并注入依赖。这种方式解决了循环依赖问题,因为依赖的Bean不会立即初始化,而是在需要时才初始化。

使用@Lazy注解的关键在于延迟初始化Bean,确保BeanA和BeanB都已实例化但尚未注入依赖。然后,Spring容器在需要时才初始化它们,并注入依赖,避免了循环依赖的问题。

潜在问题:

  • 延迟初始化可能会导致性能问题,特别是在应用程序需要大量Bean时。因此,需要根据具体情况谨慎使用@Lazy注解。

2.3 三级缓存机制和使用构造函数、@Lazy、@Setter来解决循环依赖有什么区别?

2.3.1 区别

三级缓存机制:

  1. 区别: 三级缓存机制是Spring框架内部的解决方案,用于处理复杂的循环依赖情况。它通过一级缓存、二级缓存和三级缓存来管理Bean的创建和初始化。

  2. 使用情况: 主要在Spring框架内部使用,对开发者来说是透明的。适用于解决长链或多个Bean之间存在复杂的循环依赖关系的情况。

构造函数注入:

  1. 区别: 构造函数注入是一种手动的解决方法,通过在Bean的构造函数中定义依赖关系,确保Bean的依赖在创建时得到满足。

  2. 使用情况: 适用于相对简单的循环依赖情况,其中Bean的依赖可以通过构造函数解决。通常适用于Bean之间的强依赖关系。

@Lazy注解:

  1. 区别: @Lazy注解告诉Spring容器要延迟初始化Bean,以避免立即解决循环依赖。Bean只在需要时初始化。

  2. 使用情况: 适用于简单的循环依赖情况,其中Bean的依赖解决较为灵活,延迟初始化不会导致性能问题。适用于那些不需要立即初始化的Bean。

@Setter注解:

  1. 区别: @Setter注解使用Setter方法注入Bean之间的依赖。Spring容器会在实例化Bean时创建Bean的原始对象,然后使用Setter方法注入依赖。

  2. 使用情况: 适用于简单的循环依赖情况,可以通过Setter方法灵活地解决Bean之间的依赖问题,也适用于Bean之间的松散依赖关系。

总结:

  • 三级缓存机制是Spring框架内部的解决方案,适用于复杂的循环依赖情况。
  • 构造函数注入适用于相对简单的循环依赖情况,适合强依赖关系的Bean。
  • @Lazy注解适用于简单的循环依赖情况,适合不需要立即初始化的Bean。
  • @Setter注解适用于简单的循环依赖情况,适合灵活的依赖解决和松散依赖关系的Bean。选择方法取决于具体的应用场景和代码结构。

2.3.2 ​为什么有了三级缓存机制还要有构造函数、@setter和@lazy方法来解决循环依赖?

​虽然Spring的三级缓存机制是一种强大的内部解决方案,但在某些情况下,使用构造函数、@Setter和@Lazy等手动方法来解决循环依赖问题可以提供更多的控制和适应性,同时提高代码的可读性和性能。选择哪种方法通常取决于具体的应用场景和开发者的偏好。所以,尽管Spring的三级缓存机制可以有效解决循周期依赖问题,但有构造函数、@Setter和@Lazy等其他方法来解决循环依赖问题的原因有以下几点:

  1. 灵活性和可读性: 使用构造函数、@Setter和@Lazy等方法可以使代码更具灵活性和可读性,更容易理解和维护。这些方法允许开发者明确指定依赖关系,而不依赖Spring的内部机制,使代码更直观。

  2. 三级缓存不适用于所有场景: 三级缓存机制是Spring框架的内部实现,对于大多数情况下,它能够有效解决循环依赖问题。然而,对于某些特定情况,可能需要更多的控制权和定制化,以满足应用程序的需求。构造函数、@Setter和@Lazy提供了更多灵活的选项。

  3. 性能问题: 三级缓存机制虽然是一种强大的机制,但在某些情况下可能会引入性能开销。特别是对于具有大量Bean的应用程序,三级缓存机制可能会导致内存占用问题。构造函数、@Setter和@Lazy等方法可以在需要时更精细地控制Bean的初始化,以减少性能开销。

  4. 多模块项目: 在多模块的大型项目中,不同模块之间的依赖关系可能更加复杂,可能需要更多的手动干预来管理循环依赖。使用构造函数、@Setter和@Lazy等方法可以更好地适应不同模块之间的差异。

3. 总结

在Spring中,循环依赖是指两个或多个Bean之间存在相互依赖的情况,通常出现在BeanA依赖BeanB,同时BeanB也依赖BeanA的情况。这种循环依赖会导致Spring容器无法正确初始化这些Bean,从而引发循环依赖异常。

为了解决循环依赖问题,Spring采用了多种机制:

  1. 三级缓存机制:Spring的三级缓存(一级缓存、二级缓存、三级缓存)用于解决循环依赖问题。它通过提前将Bean工厂的lambda表示式存储在三级缓存中,确保依赖Bean在创建时可用,从而避免了循环依赖问题。这种机制保证了Bean的正确初始化,同时避免了死锁和启动失败。

  2. 构造函数注入:使用构造函数注入来解决循环依赖问题,确保Bean的依赖在Bean完成初始化之前已经准备好。构造函数注入是通过在Bean的构造函数中传入依赖的Bean来实现的,这保证了Bean的依赖在初始化时已经可用。

  3. Setter方法注入:使用Setter方法注入也是一种常见的方法,它允许Bean在运行时动态注入依赖,从而避免初始化时的问题。Setter方法注入通过在Bean的Setter方法中注入依赖来实现,确保Bean的依赖在初始化后注入。

  4. @Lazy注解:@Lazy注解告诉Spring容器要延迟初始化Bean,以避免循环依赖问题。这种方式延迟依赖注入,确保Bean的依赖在需要时才初始化,从而解决了循环依赖问题。

每种方法都有其适用的场景和潜在问题,需要根据具体情况选择最合适的解决方案。解决循环依赖问题是Spring容器的一个关键功能,它确保了Bean的正确初始化和依赖注入,使Spring成为一个功能强大的依赖注入容器。

相关推荐
安全系统学习1 分钟前
【网络安全】Qt免杀样本分析
java·网络·安全·web安全·系统安全
java干货21 分钟前
虚拟线程与消息队列:Spring Boot 3.5 中异步架构的演进与选择
spring boot·后端·架构
一只叫煤球的猫25 分钟前
MySQL 8.0 SQL优化黑科技,面试官都不一定知道!
后端·sql·mysql
SoFlu软件机器人27 分钟前
智能生成完整 Java 后端架构,告别手动编写 ControllerServiceDao
java·开发语言·架构
写bug写bug1 小时前
如何正确地对接口进行防御式编程
java·后端·代码规范
Cyanto1 小时前
Java并发编程面试题
java·开发语言·面试
不超限2 小时前
Asp.net core 使用EntityFrame Work
后端·asp.net
在未来等你2 小时前
互联网大厂Java求职面试:AI大模型与云原生技术的深度融合
java·云原生·kubernetes·生成式ai·向量数据库·ai大模型·面试场景
豌豆花下猫2 小时前
Python 潮流周刊#105:Dify突破10万星、2025全栈开发的最佳实践
后端·python·ai