Java WEB技术-什么是循环依赖?常用解决循环依赖的方法有哪些?(Spring三级缓存,@Lazy注解,设计修改,依赖解耦等)

1、什么是循环依赖

环依赖指的是多个对象之间相互依赖,形成一个依赖的闭环。
例如:

  • 类 A 依赖于类 B。
  • 类 B 又依赖于类 A。

循环依赖代码示例:

@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;
    }
}

说明:
在创建B对象时,需要注入A对象的实例。在创建A对象时,又需要注入B对象的实例。这样在创建A和B时形成了闭环,也就是循环依赖了。

2、循环依赖产生原因

(1)业务逻辑设计问题

  • 紧耦合:类之间的职责划分不清晰,导致相互依赖。
  • 过度封装:为了实现某些功能,将过多的责任分配给单一类,从而导致依赖复杂化。
    示例:
    比如在一个电商系统中,订单服务类需要调用商品服务类来获取商品信息,而商品服务类又可能需要调用订单服务类来统计商品的销售订单数量,这样就可能产生循环依赖。

(2)框架特性使用不当

  • 构造器注入:如果两个 Bean 使用构造器注入的方式相互依赖,Spring 容器无法正确解析它们。
  • 单例模式:在单例 Bean 的场景下,Spring 提供了某些机制来解决循环依赖,但这些机制也有局限性。

归根结底:
就是代码设计不合理。

3、解决循环依赖的方案

(1)、修改设计(推荐)

重构代码:通过引入中间类或接口,打破循环依赖。

  • 将公共逻辑提取到一个独立的类中。
  • 使用接口或抽象类解耦。

示例代码:

// 引入接口 C 来解耦
public interface C {
    void doSomething();
}

// 类 A 依赖接口 C
public class A implements C {
    private C c;

    public A(C c) {
        this.c = c;
    }

    @Override
    public void doSomething() {
        System.out.println("A is doing something");
    }
}

// 类 B 实现接口 C
public class B implements C {
    private C c;

    public B(C c) {
        this.c = c;
    }

    @Override
    public void doSomething() {
        System.out.println("B is doing something");
    }
}

解释:
将A对B的依赖方法或B对A的依赖方法抽取出来,放到C类中。使A和B直接依赖于C,而不是相互依赖。

(2)、使用Spring的Setter注入(推荐)

Spring 利用三级缓存来解决 setter 注入的循环依赖问题。
说明:
setter注入,字段注入,方法参数注入等,Spring都会通过三级缓存的方式解决循环依赖问题。但构造器注入无法解决循环依赖问题。
Java WEB技术-什么是循环依赖?常用解决循环依赖的方法有哪些?(Spring三级缓存,@Lazy注解,设计修改,依赖解耦等)_第1张图片

三级缓存分别是:

  • 一级缓存(singletonObjects):
    • 存储已经完全初始化并完成依赖注入的单例 Bean。
    • 这些Bean可以直接从一级缓存中获取,无需重新创建。
    • 这是一个Map类型的缓存。
  • 二级缓存(earlySingletonObjects):
    • 存储尚未完成初始化但已经创建的对象实例。
    • 当某个Bean正在初始化时,如果其他Bean需要它,可以从二级缓存中获取一个“早期引用”(Early Reference)。
    • 这是一个Map类型的缓存。
  • 三级缓存(singletonFactories):
    • 存储对象工厂(ObjectFactory),用于延迟创建对象实例。
    • 如果某个Bean尚未创建,Spring会先将ObjectFactory放入三级缓存,以便后续根据需要动态创建对象。
    • 这是一个Map>类型的缓存

示例代码:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
class A {
    @Autowired
    private B b;

    public B getB() {
        return b;
    }
}

@Component
class B {
    @Autowired
    private A a;

    public A getA() {
        return a;
    }
}

三级缓存的工作原理:

  • 创建Bean A时,首先会创建A的ObjectFactory对象放入三级缓存中,然后使用ObjectFactory创建A对象,并将其放入到二级缓存中。然后进行A对象的属性填充,此时发现需要注入Bean B对象。
  • Spring先在一级缓存中查找B对象的实例,没有找到;在去二级缓存中查找B对象的早期实例,仍然没有找到;此时会终止A的属性注入过程,先去创建Bean B对象。
  • 创建Bean B时,首先也会创建B的ObjectFactory对象放入三级缓存中,然后使用B的ObjectFactory创建B对象,并将其放入到二级缓存中。然后进行B对象的属性填充,此时发现需要注入Bean A对象。
  • Spring先在一级缓存中查找A对象的实例,没有找到;再去二级缓存中查找A对象的早期实例,此时在二级缓存中找到了A对象的早期实例,将其注入到Bean B对象中,完成B对象的属性填充。接着将成品B对象加入到一级缓存中。
  • Bean B对象创建完成后,Spring会重新回到Bean A的属性填充过程,再一次从一级缓存和二级缓存中查找B对象的实例,当在一级缓存中查找时,成功找到了B对象的实例,将其注入到A对象中,完成A对象的属性填充。接着在将A对象放入一级缓存中。
  • 至此,A对象和B对象都创建完成且属性填充成功,都位于Spring的以及缓存中。

(3)、使用延迟加载(@Lazy注解)

在Spring中,可以使用@Lazy注解延迟加载依赖,避免在初始化阶段形成循环依赖。

示例代码:

@Component
public class A {
    private B b;

    @Autowired
    public A(@Lazy B b) {
        this.b = b;
    }
}

@Component
public class B {
    private A a;

    @Autowired
    public B(@Lazy A a) {
        this.a = a;
    }
}

@lazy的原理说明:
在Spring容器中,当使用@Lazy注解来处理循环依赖时,Bean的初始化行为会发生一些变化。
具体执行如下:

  1. Bean注册阶段:首先,Spring容器会扫描组件并注册所有被@Component或通过其他方式(如Java配置类中的@Bean方法)定义的Bean。在这个例子中,BeanA和BeanB都会被识别并注册到Spring的上下文中。
  2. Bean实例化阶段:
    • 当Spring尝试实例化BeanA时,它需要注入一个BeanB的实例。
    • 由于BeanB的构造函数参数上标记了@Lazy,Spring不会立即创建一个真正的BeanB实例,而是创建一个代理对象(通常是CGLIB代理),这个代理会在第一次实际访问beanB的方法或属性时触发BeanB的真实实例化。
    • 同样地,当Spring尝试实例化BeanB时,它需要注入一个BeanA的实例。此时,BeanA已经被完全实例化(尽管它的beanB字段指向的是一个代理对象而不是真实的BeanB实例),所以可以直接注入给BeanB。
  3. 容器启动完成后的状态:
    • 在Spring容器启动完成后,BeanA和BeanB都被注册到了Spring上下文中。
    • BeanA持有对BeanB的一个代理引用,而不是直接持有BeanB的实际实例。
    • BeanB持有对BeanA的实际引用。
  4. 首次访问beanB的行为:
    • 当你在BeanA内部第一次调用beanB的任何方法或访问其属性时,Spring将触发BeanB的实际实例化过程。
    • 此时,如果BeanB本身没有其他未解决的依赖问题,那么BeanB会被正常初始化,并且后续对该代理的所有调用都将直接转发给这个真实的BeanB实例。

简单理解:
Spring仍然会在启动时,执行Bean A和Bean B的创建过程。A制作成半成品时就结束了,A对象内的B属性没有真正的填充。B是完整品,创建完成,且属性注入完成。

具体流程:

  • Step 1: Spring容器开始初始化BeanA。
  • Step 2: 为了初始化BeanA,Spring需要提供一个BeanB实例。但由于BeanB被标记为@Lazy,Spring返回的是一个代理对象而非实际的BeanB实例。
  • Step 3: 使用这个代理对象,Spring成功完成了BeanA的初始化。
  • Step 4: 接下来,Spring尝试初始化BeanB,这时它需要BeanA的实例,而BeanA已经完全初始化好了,因此可以直接提供给BeanB。
  • Step 5: 在BeanA内部首次访问beanB的任何成员之前,BeanB的实际实例不会被创建。一旦首次访问发生,Spring才会真正创建并初始化BeanB。

但是:
通过@Lazy注解的方式,虽然解决了BeanA和BeanB之间存在循环依赖问题。但由于BeanB是懒加载的,懒加载的代理机制意味着BeanB的初始化会被推迟到第一次实际使用时,这可能会带来额外的延迟。
通常在使用中并不推荐使用@Lazy注解的方式,这种方法仅适用于简单的循环依赖情况,对于复杂的依赖关系,还是应该考虑重构代码以减少耦合度或者采用不同的设计模式才是上策。

(4)、手动避免循环依赖的方法

  • 重构代码:重新设计类的结构,将相互依赖的逻辑提取到一个新的类中,避免直接的循环依赖。例如,可以创建一个中间服务类来处理A和B之间的交互。
  • 使用事件驱动:通过事件机制来解耦相互依赖的类。一个类发布事件,另一个类监听事件并做出相应的处理,而不是直接相互依赖。
  • 依赖注入优化:仔细规划依赖注入的方式和顺序,避免不必要的依赖关系,确保类之间的依赖关系清晰、简单。

4、四种解决方案对比

Java WEB技术-什么是循环依赖?常用解决循环依赖的方法有哪些?(Spring三级缓存,@Lazy注解,设计修改,依赖解耦等)_第2张图片

向阳而生,Dare To Be!!!

你可能感兴趣的:(java杂货铺,java,开发语言,spring)