Spring的循环依赖

696 阅读5分钟

1. Spring的循环依赖引发的问题是什么?

Spring 是一个开源的,为 Java 应用程序提供了全面的基础结构支持的框架。Spring 框架对依赖注入(Dependency Injection,DI)的支持是其核心功能之一。

循环依赖(Circular Dependency)在 Spring 中是指两个或者更多的 bean 彼此依赖对方,形成一个闭环。这种依赖关系在对象创建时,可能会导致一系列问题。

在理解 Spring 中的循环依赖问题之前,我们需要理解 Spring 容器是如何创建和管理 bean 的。Spring 容器在初始化 bean 时,会先实例化对象,然后再进行依赖注入。

  1. 对象创建问题:例如,如果 A 依赖于 B,B 又依赖于 A,在创建 A 的时候,我们需要首先创建 B,但在创建 B 的时候,我们又需要创建 A。这就形成了一个无限循环,导致 StackOverflowError。
  2. 属性赋值问题:在循环依赖的情况下,由于涉及的 bean 之间存在依赖,当尝试注入这些依赖时,如果相应的 bean 还未完全初始化(即还未进行属性赋值),就可能会注入一个未完全初始化的 bean,这就可能导致运行时的问题。

Spring 框架通过使用三级缓存和延迟加载的策略来解决单例模式下的循环依赖问题,但是对于 prototype scope 的 bean,Spring 框架则无法解决循环依赖的问题。

避免循环依赖的最好方式是进行良好的设计,避免出现循环依赖的情况。这包括使用接口而非实现进行依赖,利用设计模式(如观察者模式,中介者模式等)去解耦合等策略。

2. Spring是如何解决循环依赖的?

Spring 框架主要通过三级缓存机制来解决单例模式下的循环依赖问题。

以下是这个过程的简单描述:

  1. 一级缓存(Singleton Objects) :一级缓存也被称为单例对象的缓存,它用于存储完全初始化的 Bean。当一个 Bean 完全创建完成后,Spring 会将其放入一级缓存中。
  2. 二级缓存(Early Singleton Objects) :二级缓存也被称为早期单例对象的缓存,它用于存储已实例化但还未完全初始化(属性还未完全设置)的 Bean。
  3. 三级缓存(Singleton Factories) :三级缓存存储的是一个特殊的工厂对象,该工厂对象用于生成一个早期的 Bean 对象。当有循环依赖的情况发生时,Spring 会通过这个工厂对象来生成一个提前暴露的 Bean。

当 Spring 在创建 Bean 的过程中遇到循环依赖的情况时,如 A 依赖于 B,而 B 又依赖于 A,这个时候就会触发三级缓存机制:

  1. Spring 在创建 A 的时候,发现 A 依赖于 B,于是暂停创建 A,开始创建 B。
  2. 在创建 B 的过程中,发现 B 依赖于 A,但此时 A 还没有被完全创建,于是 Spring 从二级缓存中获取 A 的早期引用,并注入到 B 中,然后继续完成 B 的创建过程,最后将完全初始化的 B 放入一级缓存中。
  3. Spring 回过头来继续创建 A,此时 B 已经被创建完成,因此可以直接注入到 A 中,然后完成 A 的创建过程,并将 A 放入一级缓存中。

以上就是 Spring 框架解决循环依赖问题的基本机制。然而,需要注意的是,这种机制只适用于单例作用域的 Bean。对于原型作用域的 Bean,Spring 框架并不能解决循环依赖的问题,因为每次获取原型作用域的 Bean,Spring 容器都会创建一个新的实例,这就导致 Spring 无法管理这种类型的 Bean 之间的循环依赖问题。在面对这种情况时,需要通过改进设计来避免循环依赖的发生。

3. 如何通过代码解决Spring循环依赖的问题?

循环依赖的问题通常是由于设计不合理导致的,可以通过合理的设计来避免,如下面这些方式:

  1. 通过接口解耦:将依赖关系限制在接口或抽象类上,而不是具体的实现类。
public interface ServiceA {
    void methodA();
}

public interface ServiceB {
    void methodB();
}

@Service
public class ServiceAImpl implements ServiceA {
    private final ServiceB serviceB;

    public ServiceAImpl(ServiceB serviceB) {
        this.serviceB = serviceB;
    }

    @Override
    public void methodA() {
        serviceB.methodB();
        // some logic here
    }
}

@Service
public class ServiceBImpl implements ServiceB {
    private final ServiceA serviceA;

    public ServiceBImpl(ServiceA serviceA) {
        this.serviceA = serviceA;
    }

    @Override
    public void methodB() {
        serviceA.methodA();
        // some logic here
    }
}
  1. 通过 setter 注入:使用 setter 方法进行依赖注入,而不是构造器注入。但这种方式可能导致对象的状态在一段时间内是不完整的。
@Service
public class ServiceA {
    private ServiceB serviceB;

    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }

    public void methodA() {
        serviceB.methodB();
        // some logic here
    }
}

@Service
public class ServiceB {
    private ServiceA serviceA;

    @Autowired
    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }

    public void methodB() {
        serviceA.methodA();
        // some logic here
    }
}
  1. 使用 @Lazy 注解:@Lazy 注解可以用于延迟初始化一个 bean,这样可以在一定程度上解决循环依赖的问题。需要注意的是,这种方法也可能导致对象状态在一段时间内是不完整的。
@Service
public class ServiceA {
    private final ServiceB serviceB;

    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }

    public void methodA() {
        serviceB.methodB();
        // some logic here
    }
}

@Service
public class ServiceB {
    private final ServiceA serviceA;

    public ServiceB(@Lazy ServiceA serviceA) {
        this.serviceA = serviceA;
    }

    public void methodB() {
        serviceA.methodA();
        // some logic here
    }
}
  1. 使用观察者模式、中介者模式等设计模式:在合适的情况下,通过设计模式可以解耦合两个类之间的直接依赖关系,从而避免循环依赖的问题。

总的来说,解决循环依赖的问题主要靠良好的设计。如果代码中出现循环依赖,通常表明设计存在问题,需要重新审视代码的组织和依赖关系。