自学内容网 自学内容网

Spring如何处理循环依赖

1. BeanFactory 和 ApplicationContext

  • BeanFactory 是最基础的IoC容器,提供了基本的bean加载和生命周期管理功能。
  • ApplicationContext 继承自BeanFactory,并且增加了更多的企业级特性,如事件传播、国际化支持等。它也是大多数Spring应用中使用的顶级接口。

2. Bean 的生命周期阶段

Spring bean的生命周期分为几个明确的阶段,每个阶段都有相应的钩子方法或接口供开发者扩展:

  • 实例化(Instantiation):通过调用构造函数或工厂方法来创建bean的实例。
  • 属性填充(Population of properties):为bean注入其所需的依赖项(其他bean或值)。这是最容易出现循环依赖的地方。
  • Aware 接口回调:如果bean实现了特定的Aware接口(如BeanFactoryAwareApplicationContextAware等),则在这个阶段调用相应的setter方法。
  • BeanPostProcessor前置处理:所有实现了BeanPostProcessor接口的类都可以在此阶段对bean进行预处理。
  • 初始化前处理:执行InitializingBean接口中的afterPropertiesSet()方法或自定义的init-method
  • BeanPostProcessor后置处理:再次提供给BeanPostProcessor接口实现者一个机会,在bean完全初始化之后对其进行修改。
  • 使用:bean现在是完全可用的状态,并且可以被其他组件引用。
  • 销毁:当容器关闭时,调用DisposableBean接口的destroy()方法或自定义的destroy-method

3. 循环依赖解决方案的技术细节

三级缓存机制

Spring利用了三个不同的缓存来管理和解决循环依赖问题:

  • singletonObjects (一级缓存): 存储已经完成初始化的单例bean。这些bean是可以安全地用于其他bean的依赖注入。
  • earlySingletonObjects (二级缓存): 存储尚未完成初始化但至少已经被实例化的单例bean。这个缓存允许提前暴露bean的部分状态,从而使得其他bean可以在自己的初始化过程中获取到该bean的一个早期版本。
  • registeredSingletons (三级缓存): 记录正在创建中的bean的名字,以避免重复创建同一个bean。这有助于确保即使存在循环依赖,也不会导致无限递归。

解决循环依赖的具体过程

假设我们有两个bean AB 形成循环依赖,即 A 依赖于 B,而 B 也依赖于 A。以下是更加详细的处理步骤:

// 定义两个互相依赖的bean
public class A {
    private B b;

    @Autowired
    public void setB(B b) {
        this.b = b;
        System.out.println("A's setter for B called");
    }

    // Other methods...
}

public class B {
    private A a;

    @Autowired
    public void setA(A a) {
        this.a = a;
        System.out.println("B's setter for A called");
    }

    // Other methods...
}
<!-- 配置文件 applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.example.A">
        <property name="b" ref="b"/>
    </bean>

    <bean id="b" class="com.example.B">
        <property name="a" ref="a"/>
    </bean>
</beans>

处理流程

  1. 解析并创建A:当Spring尝试创建bean A 时,发现它依赖于bean B。此时,Spring会先将A放入registeredSingletons集合中,表示A正在创建中。
  2. 开始创建B:接下来,Spring尝试创建bean B,并发现B依赖于A。由于A已经在registeredSingletons中,Spring知道A正在创建中,但是还没有完成初始化。
  3. 提前暴露A:为了避免死锁,Spring会检查earlySingletonObjects缓存。如果A存在于这个缓存中,那么Spring会从这里获取A的一个早期暴露对象,并将其提供给B作为依赖。对于某些场景,如代理模式下的AOP,Spring可能会创建一个代理对象来代替真正的A
  4. 完成B的初始化B获得了A的引用后,继续完成自己的初始化过程,包括属性设置、初始化方法调用等。一旦B完成初始化,它就会被放置到singletonObjects中,并从registeredSingletons移除。
  5. 完成A的初始化:回到A的创建过程,因为B现在已经准备好并且存在于singletonObjects中,所以A可以直接从那里获取到完整的B实例,进而完成自己的初始化。最后,A也会被放入singletonObjects中,并从earlySingletonObjects移除。
  6. 移除临时缓存:随着AB都完成了各自的初始化,它们将不再出现在registeredSingletonsearlySingletonObjects中,这意味着这两个bean都已经成功创建并可用了。
输出结果

当你运行这段代码时,输出可能是这样的:

A's setter for B called
B's setter for A called

这表明A首先被创建并部分初始化(即设置了对B的引用),然后B被创建并完全初始化(即设置了对A的引用)。最后,A也被完全初始化。

处理构造器注入的限制

对于构造器注入而言,由于构造器必须在所有依赖都准备好之后才能调用,因此Spring无法自动处理由构造器注入引起的循环依赖。这是因为构造器注入要求所有的依赖项在构造函数被调用之前都是已知且可用的。如果存在循环依赖,这就可能导致无限递归或者BeanCurrentlyInCreationException异常。

例如,如果我们修改上面的例子,让AB都使用构造器注入:

public class A {
    private final B b;

    @Autowired
    public A(B b) {
        this.b = b;
        System.out.println("A constructor called with B");
    }

    // Other methods...
}

public class B {
    private final A a;

    @Autowired
    public B(A a) {
        this.a = a;
        System.out.println("B constructor called with A");
    }

    // Other methods...
}

在这种情况下,启动应用程序将会导致错误,因为Spring无法解决这种类型的循环依赖。

要解决这个问题,可以采取以下几种策略:

  • 使用Provider模式:让依赖方持有对依赖的ObjectFactoryProvider接口的引用,而不是直接持有依赖本身。这样,实际的依赖获取可以在运行时延迟进行。下面是一个例子:
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {
    private final ObjectFactory<B> bFactory;

    @Autowired
    public A(ObjectFactory<B> bFactory) {
        this.bFactory = bFactory;
        System.out.println("A constructor called with ObjectFactory<B>");
    }

    public void doSomething() {
        B b = bFactory.getObject();
        System.out.println("A is doing something with B");
    }
}

@Component
public class B {
    private final A a;

    @Autowired
    public B(A a) {
        this.a = a;
        System.out.println("B constructor called with A");
    }

    // Other methods...
}

在这个例子中,A不再直接依赖于B,而是依赖于一个ObjectFactory<B>,这允许A在需要的时候才去获取B的实例,避免了构造器注入导致的循环依赖问题。

  • 重构代码:尽量避免设计上的循环依赖,通过重构代码结构来消除不必要的循环关系。
  • 使用setter或字段注入:虽然这并不总是最佳实践,但在某些情况下,为了快速解决问题,可以考虑改用setter或字段注入,因为它们可以与Spring的循环依赖解决方案兼容。
@Component
public class A {
    private B b;

    @Autowired
    public void setB(B b) {
        this.b = b;
        System.out.println("A's setter for B called");
    }

    // Other methods...
}

@Component
public class B {
    private A a;

    @Autowired
    public void setA(A a) {
        this.a = a;
        System.out.println("B's setter for A called");
    }

    // Other methods...
}

4. 关键类和技术

  • DefaultSingletonBeanRegistry:这是Spring内部用来管理单例bean的核心类之一,它包含了上述提到的三级缓存。它负责管理单例bean的整个生命周期,包括创建、初始化和销毁。
  • AbstractAutowireCapableBeanFactory:这是一个抽象类,实现了AutowireCapableBeanFactory接口,提供了依赖注入的支持。它是Spring中负责bean创建和初始化的主要类。它还包含了许多重要的方法,如createBean(),用于创建和初始化bean。
  • InstantiationAwareBeanPostProcessor:这是一个接口,允许在bean实例化前后进行额外的操作。例如,它可以用于创建代理对象来提前暴露bean。具体来说,SmartInstantiationAwareBeanPostProcessor是一个更高级的版本,它不仅可以在bean实例化前后操作,还可以预测最终的bean类型。
  • SmartInstantiationAwareBeanPostProcessor:继承自InstantiationAwareBeanPostProcessor,提供了更高级的功能,比如预测最终的bean类型。这对于某些场景特别有用,如需要在bean实例化之前确定其实际类型。
  • AOP代理:当涉及到面向切面编程(AOP)时,Spring会为需要代理的bean创建代理对象。这些代理对象可以在bean未完全初始化的情况下提前暴露给其他bean,从而帮助解决循环依赖的问题。

5. 总结

Spring通过三级缓存和依赖检查机制有效地解决了大部分由setter注入或字段注入导致的循环依赖问题。然而,对于构造器注入的情况,由于其实现原理的不同,Spring默认不支持这种类型的循环依赖,除非采取额外措施。同时,应该注意到,尽管Spring提供了强大的依赖管理和循环依赖处理能力,但这不应该成为滥用循环依赖的理由;良好的软件设计应尽量减少甚至避免循环依赖,以保证系统的清晰性和可维护性。


原文地址:https://blog.csdn.net/m0_50742275/article/details/144727684

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!