Java Spring 循环依赖解析

目录
  • 1、常见问题
  • 2、什么是循环依赖?
  • 3、循环依赖说明
  • 4、BeanCurrentlyInCreationException
  • 5、依赖注入的两种方式
    • 方式一:构造器方式注入依赖
  • 方式二:以 set 方式注入依赖
  • 6、Spring 三级缓存介绍和循环依赖解决过程
    • 三级缓存介绍
    • 实例化/初始化定义
    • 三级缓存使用过程
    • A/B 两对象在三级缓冲的迁移说明
    • ObjectFactory 接口
    • DEBUG 断点调试
    • 循环依赖解决
  • 7、Spring 循环依赖总结

1、常见问题

  • 你解释一下 spring 中的三级缓存?
  • 三级缓存分别是什么?三个 Map 有什么异同?
  • 什么是循环依赖?请谈谈?你看过 spring 的源码吗?一般我们说的是 spring  容器是什么?
  • 多例的情况下,循环依赖问题为什么无法解决?

2、什么是循环依赖?

多个 bean 之间相互依赖,形成闭环。比如:A 依赖于 B, B 依赖于 C , C 依赖于 A

示例代码:

public class T1 {

  class A {
    B b;
  }
  class B {
    C c;
  }
  class C {
    A a; 
  }
}

比如:A 依赖于 B, B 依赖于 C , C 依赖于 A
通常来说,如果问 Spring 容器内部如何解决循环依赖,一定是指默认的单例 Bean 中, 属性相互引用的场景。

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private C c;
}
 
@Component
public class C {
    @Autowired
    private A a;
}

3、循环依赖说明

官方说明:

参考官方说

构造方法注入:不支持循环依赖。
我们 AB 循环依赖问题。只要 A 的方式是 setter 且 singleton
就不会有循环依赖问题。

4、BeanCurrentlyInCreationException

循环依赖异常的定义如下所示,如果出现循环依赖,我们在启动/运行过程中会报这个错误。

5、依赖注入的两种方式

方式一:构造器方式注入依赖

@Component
public class ServiceA{

    private ServiceB serviceB;
        
    public ServiceA(ServiceB serverB) {
       this.serivceB = serviceB;
    }     
}

@Component
public class ServiceB{

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

构造器循环依赖是无法解决的,你想让构造器注入支持循环依赖,是不可能的。

方式二:以 set 方式注入依赖

@Component
public class ServiceA{

    private ServiceB serviceB;
        
    public setServiceB(ServiceB serverB) {
       this.serivceB = serviceB;
    }     
}

@Component
public class ServiceB{

    private ServiceA serviceA;
        
    public setServiceB(ServiceA serviceA) {
       this.serviceA = serviceA;
    }     
}

案例演示(基于 Spring 容器的循环依赖)

普通的 Java 基础编码A  类、B 类

@Data
public class A{
  private B b;
  
  public A() {
     System.out.println("----- A create success");
  }
}

@Data
public class B{
  private a a;

  public B() {
     System.out.println("------ B create success");
  }    
}

循环依赖解决

A a = new A();
B b = new B();

a.setB(b);
b.setA(a);

基于 Spring 容器的循环依赖

  • 默认的单例(singleton)的场景是支持循环依赖的,不报错
  • 原型(prototype)的场景是不支持循环依赖的, 会报错

代码演示:

循环依赖代码:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class A {

    @Autowired
    private B b;
}

@Component
public class B {

    @Autowired
    private A a;
}

默认单例,修改为原型

// 增加注解
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

一段测试程序

class BTest {

    @Configuration
    @Import({A.class, B.class})
    public static class TestConfig {

    }

    @Test
    public void currentlyincreation() {
        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(TestConfig.class);
        // 这里要获取 bean 一下,如果不去主动获取,可能是由于惰性加载没有执行,不会报错
        A a = applicationContext.getBean(A.class);
        System.out.println(a);
    }
}

循环依赖异常

6、Spring 三级缓存介绍和循环依赖解决过程

三级缓存介绍

核心类: DefaultSingletonBeanRegistry

第一级缓存:(也叫单例池)singletonObjects: 存放已经经历完整生命周期的 Bean 对象
第二级缓存:earlySingletonObjects: 存放早起暴露出来的 Bean 对象, Bean 的生命周期未结束(属性还未填充完成)
第三级缓存:Map<String, ObjectFactory<?>> singletonFactories可以存放 Bean 工厂。
只有单例 Bean 会通过三级缓存提前暴露出来解决循环依赖问题,而非单例的 bean, 每次从容器获取都是新的对象,都会重新创建,所以非单例的 bean 是没有缓存的,不会放到三级缓存中。

实例化/初始化定义

实例化/初始化

1、实例化:内存中申请一块内存空间;(比如:租赁好好房子,自己的家具沙发,床还没有搬进去。)
2、初始化属性填充:完成各种属性的赋值;(比如:装修,家电家具进场)

三级缓存使用过程

3 个 map 的四大方法,总体相关对象

第一层:(也叫单例池)singletonObjects: 存放已经初始化好的 Bean。
第二层:earlySingletonObjects: 存放的是实例化的了, 但是未初始化的 Bean。
第三层:Map<String, ObjectFactory<?>> singletonFactories
存放的是 FactroyBean。假如 A 类实现了 FactoryBean, 那么依赖注入的时候不是 A 类,而是 A类的 FAC天FactoryBean。

/**
 * 单例对象的缓存:bean 名称--Bean 实例, 即:所有的单例池
 * 表示经历了完整生命周期的 Bean 对象
 * <b>第一级缓存</b>
 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/**
 * 单例工厂的高速缓存:bean 名称--ObjectFacotry
 * 表示存放生成的 bean 工厂
 * <b>第三级缓存</b>
 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/**
 * 早起的单例对象的高速缓存:bean 名称--Bean 实例
 * 表示 Bean 的生命周期还没有走完(Bean 的属性还未填充)就把这个 Bean 存入该缓存中
 * 也就是实例化的 bean 放入了该缓存中
 * <b>第二级缓存</b>
 */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

A/B 两对象在三级缓冲的迁移说明

1、创建 A 过程中需要 B, 于是 A 将自己放入到三级缓存里面,去实例化 B
2、B 实例化的时候发现需要 A ,于是 B 先查一级缓存,没有,再查二级缓存,还是没有再查三级缓存,找到了 A 然后把三级缓存里面的这个 A 放入到二级缓存里面,并且删除三级缓存里面的 A。
3、 B 顺利初始化完毕后,将自己放入到一级缓存里面(此时 B 里面的 A 依然是创建中状态)后来接着创建 A, 此时 B 已经创建结束,直接从一级缓存里面拿到 B,然后完成创建,并且将 A 自己放到一级缓存中

ObjectFactory 接口

@FunctionalInterface
public interface ObjectFactory<T> {

    /**
     * Return an instance (possibly shared or independent)
     * of the object managed by this factory.
     * @return the resulting instance
     * @throws BeansException in case of creation errors
     */
    T getObject() throws BeansException;

}

DEBUG 断点调试

1、Bean 创建

2、放入三级缓存

3、属性填充(就是做属性注入)

循环依赖解决

整理流程梳理:

1、调用 doGetBean() 方法 , 想要获取 beanA , 于是调用 getSingletion() 方法从缓存中查询 beanA
2、在 getSingletion() 方法中,从一级缓存中查找,没有,返回 null
3、doGetBean() 犯法中获取到 beanA 为 null, 于是就走对应的处理逻辑,调用 getSIngletion() 的重载方法(参数为 ObjecFactory  的)
4、在 getSingletion() 方法中,先将 beanB_name 添加到一个集合中,用于标记该 bean 正在创建中,然后回调匿名内部类的 createBean  方法
5、进入 AbstractAutowireCapableBenaFactory#doCreateBean , 先反射调用构造器创建出 beanA 的实例,然后判断,是否为单例、是否允许提前暴露引用(对于单例一般为 true)、是否正在创建中(即是在第四步的集合中)。判断为 true 则将 beanA 添加到【三级缓存】中。
6、对 beanA 进行属性填充,此时检测到 beanA 依赖 beanB , 于是开始查找 beanB
7、调用 doGetBean 方法,和上面的 beanA 过程一样,到缓存中查找 beanB, 没有则创建,然后给 beanB 填充属性。
8、此时 beanB  依赖于 beanA , 调用 getSingleton() 获取 beanA , 依次从一级、二级、三级缓存中找,此时三级缓存中获取到 beanA 的创建工厂,通过创建工厂获取到 singletonObject, 此时这个 singletonObject 指向的就是上面的 doCreateBean() 方法实例化的 beanA 。
9、这样 beanB 就获取到了 beanA 的依赖,于是 beanB 顺利完成实例话,并将 beanA 从三级缓存移动到二级缓存中。
10、随后 beanA  继续他的属性填充工作,此时也获取到了beanB , beanA 也随之完成了创建,回调 getSingleton() 方法中继续向下执行,将 beanA 从二级缓存中移动到一级缓存中。

7、Spring 循环依赖总结

Spring 创建 Bean 主要分为两个步骤,创建原始 Bean 对象,接着去填充属性和初始化。
每次创建 Bean 之前,我们都会从缓存中查一下有没有该 bean , 因为是单例, 只能有一个
当我们创建 beanA  的原始对象之后,并且把它放入到三级缓存中,接下来就该填充属性了,这个时候发现依赖了 beanB , 就直接去创建 beanB, 同样的流程,创建完 beanB 统筹国内属性时又发现了它依赖了 beanA  又是同样的流程。

不同的是:

这个时候可以在三级缓存只能够查询到刚才放进去的原始对象 beanA , 所以不需要继续创建,用它注入 beanB , 完成 beanB 的创建
既然 beanB 创建好了,所以 beanA 就可以弯沉属性填充的步骤了,接下来执行剩下的逻辑完,完成闭环

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 从 singletonObject 获取实例,singletonObject 中的实例都是准备好的 bean 实例
    Object singletonObject = this.singletonObjects.get(beanName);
    // isSingletonCurrentlyInCreation() 判断当前单例 bean 是否正在创建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // Consistent creation of early reference within full singleton lock
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    // 一级缓存中没有就去二级缓存中找            
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 二级缓存也没有,就去三级缓存查找
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            // 三级缓存存在的化,就把它移动到二级缓存
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

Spring 解决循环依赖靠的是 Bean 的“中间态” 这个概念。而这个中间态指的是 已经实例化还没有初始化的状态 ---> 半成品,实例话的过程有是通过构造器创建的,如果 A 还没有创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决。

Spring 为了解决单例的循环依赖问题,使用了三级缓存:

  • 其中一级缓存为单例池(singletonObjects)
  • 二级缓存为提前曝光对象(earlySingletonObjects)
  • 三级缓存为提前曝光对象工厂(singletonFactories)

假设 A,B 循环引用,实例化 A 的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了 B ,同样的流程也是实例化后放入三级缓存,接着去填充属性时发现自己依赖 A,这个时候从缓存中查找到早起暴露的 A,没有 AOP 代理的化,直接将 A 原始对象注入 B,完成 B的初始化后,进行属性填充和初始化,这个时候 B完成后,就去完成剩下 A的步骤,如果有 AOP 代理,就会进行 AOP 处理获取代理后的  A,注入 B, 走剩下的流程。

到此这篇关于Java Spring 循环依赖解析的文章就介绍到这了,更多相关Spring 循环依赖解析内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java分支结构和循环结构原理与用法详解

    本文实例讲述了Java分支结构和循环结构.分享给大家供大家参考,具体如下: 流程控制分类 顺序语句:从上到下按顺序依次执行 分支语句:根据条件不同,执行不同语句 循环语句:重复执行某些动作 单分支条件判断语句 条件语句 只是单独的判断条件是否成立 if选择结构是根据条件判断之后再做处理 语法 if(布尔表达式) { //如果布尔表达式为true将执行的语句 } 注意:条件必须是boolean类型 if只带一条语句可以省略{} 双分支条件判断语句 if...else...需要对条件成立和不成立的情

  • JAVA用for循环打印空心菱形

    空心菱形如图所示 那么我们应该如何去写出来呢?这就不得不引出一个非常经典的案例,金字塔案例,菱形像不像是两个对称的金字塔呢?废话不多说了,上思路:(仅代表个人理解,欢迎指点) 1.首先我们要写出一个空心菱形,那么我们首先得把需求由繁化简,把他拆分成一个一个简单的需求, 1.1那我们就先写半个金字塔, 是不是很眼熟的结构,没错和九九乘法表类似的结构,上代码 for(int i = 1;i <= 5;i++){ //i代表层数,这个可以设置一个变量接收,随便几层都行,不一定非要是5 for(int

  • Java分支结构程序设计实例详解

    1.从键盘输入三个整数分别存入num1,num2,num3,对他们进行排序,并且从小到大输出. import java.util.Scanner; public class CompareThreeNumber { public static void main(String[] args) { Scanner scan = new Scanner(System.in); System.out.println("请输入你的第一个整数:"); int num1 = scan.nextIn

  • Java基础之switch分支结构详解

    一.基本语法 二.流程图 1.画出 swtich 出流程 2.案例说明流程图 三.快速入门 案例:Switch01.java 请编写一个程序,该程序可以接收一个字符,比如:a,b,c,d,e,f,g a 表示星期一,b 表示星期二 - 根据用户的输入显示相应的信息.要求使用 switch 语句完成 代码: /* 案例:Switch01.java 请编写一个程序,该程序可以接收一个字符,比如:a,b,c,d,e,f,g a表示星期一,b表示星期二 - 根据用户的输入显示相应的信息.要求使用 swi

  • Java的分支结构与循环你知道多少

    目录 1.continue关键字 2.双重循环 总结 1.continue关键字 continue :继续 适用场景:只能用在循环中 作用:表示跳出本次循环,继续执行下一次循环 break和continue的区别? 适用场景不同,break可以用于switch和循环中,continue只能用在循环中 作用不同: break表示中断循环,未执行完的循环次数不再执行 continue表示跳出本次循环,继续执行下一次循环 package com.qfedu.test1; /** * continue

  • Java 循环队列/环形队列的实现流程

    之前,我们使用链表实现了基础队列,链接放在这里可以去康康哟 Java栈和基础队列的实现详解 之所以没有选择数组来实现,是因为每有一个元素出队,数组中所有剩下的元素都需要向前移动一次,因此没有链表高效. 此时,我们就引出了循环队列的概念. 循环队列,又称环形队列,逻辑上是一个环,物理上依然是线性表. head-指向队列的第一个元素 tail-指向队列最后一个元素的下一个位置 当tail走到数组末尾时,下一步再次返回数组头部(从0开始) 出队之后的元素就访问不到了,此时逻辑上已经将它删除了,tail

  • Java Spring 循环依赖解析

    目录 1.常见问题 2.什么是循环依赖? 3.循环依赖说明 4.BeanCurrentlyInCreationException 5.依赖注入的两种方式 方式一:构造器方式注入依赖 方式二:以 set 方式注入依赖 6.Spring 三级缓存介绍和循环依赖解决过程 三级缓存介绍 实例化/初始化定义 三级缓存使用过程 A/B 两对象在三级缓冲的迁移说明 ObjectFactory 接口 DEBUG 断点调试 循环依赖解决 7.Spring 循环依赖总结 1.常见问题 你解释一下 spring 中的

  • Java Spring循环依赖原理与bean的生命周期图文案例详解

    前言 Spring是如何处理循环依赖的,又是怎么做到,互相注入对方的proxy bean而不是raw bean的?现在就分析一下 一.循环依赖是什么 Spring中放入两个Service,分别是C1和C2,然后C1和C2又互为对方的成员变量.这种情况C1和C2就可以说是相互循环依赖了 二.源码图解 1. bean的主要生命周期图解 上图是一个没有循坏依赖的bean的主要生命周期节点,下图的循坏依赖可以结合该图解一起看 2.循环依赖图解 可以看到里面有一个很重要的逻辑: 当一个bean经过所有的步

  • Java中的Spring循环依赖详情

    目录 什么是循环依赖? 那么循环依赖是个问题吗? Bean的生命周期 三级缓存 解决循环依赖思路分析 Spring到底解决了哪种情况下的循环依赖 总结 什么是循环依赖? 很简单,就是A对象依赖了B对象,B对象依赖了A对象. 比如: 那么循环依赖是个问题吗? 如果不考虑Spring,循环依赖并不是问题,因为对象之间相互依赖是很正常的事情. 比如: 这样,A,B就依赖上了. 但是,在Spring中循环依赖就是一个问题了,为什么? 因为,在Spring中,一个对象并不是简单new出来了,而是会经过一系

  • Spring循环依赖的三种方式(推荐)

    引言:循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错.下面说一下spring是如果解决循环依赖的. 第一种:构造器参数循环依赖 Spring容器会将每一个正在创建的Bean 标识符放在一个"当前创建Bean池"中,Bean标识符在创建过程中将一直保持 在这个池中,因此如果在创建Bean过程中发现自己已经在"当前创建Bean池"里时将抛出 BeanCurrentlyInCrea

  • 简单了解Spring循环依赖解决过程

    这篇文章主要介绍了简单了解Spring循环依赖解决过程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 说起Spring中循环依赖的解决办法,相信很多园友们都或多或少的知道一些,但当真的要详细说明的时候,可能又没法一下将它讲清楚.本文就试着尽自己所能,对此做出一个较详细的解读.另,需注意一点,下文中会出现类的实例化跟类的初始化两个短语,为怕园友迷惑,事先声明一下,本文的实例化是指刚执行完构造器将一个对象new出来,但还未填充属性值的状态,而

  • Spring循环依赖的解决办法,你真的懂了吗

    介绍 先说一下什么是循环依赖,循坏依赖即循环引用,两个或多个bean相互引用,最终形成一个环.Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成 Spring的循环依赖有两种场景 构造器的循环依赖 属性的循环依赖 构造器的循环依赖,可以在构造函数中使用@Lazy注解延迟加载.在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入 属性的循环依赖主要是通过3个map来解决的 构造器的循环依赖 @Component publi

  • Spring 循环依赖之AOP实现详情

    前言: 我们接着上一篇文章继续往下看,首先看一下下面的例子,前面的两个serviceA和serviceB不变,我们添加一个BeanPostProcessor: @Component public class MyPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansEx

  • 浅入浅出的讲解Spring循环依赖问题

    目录 前言 概念 什么是循环依赖? 报错信息 通俗版理解 两人对峙 必须有一人妥协 Spring版理解 实例化和初始化什么区别? 三级缓存 创建过程(简易版) 创建过程(源码版) 最后 前言 最近有粉丝问到了循环依赖问题,以后再有人问你,拿这篇"吊打"他. 概念 什么是循环依赖? 多个bean之间相互依赖,形成了一个闭环.比如:A依赖于B.B依赖于C.C依赖于A. 通常来说,如果问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中,基于set方法构造注入的属性互相引用的

  • 一文搞懂Spring循环依赖的原理

    目录 简介 循环依赖实例 测试 简介 说明 本文用实例来介绍@Autowired解决循环依赖的原理.@Autowired是通过三级缓存来解决循环依赖的. 除了@Autoired,还有其他方案来解决循环依赖的,见:Spring循环依赖的解决方案详解 概述 @Autowired进行属性注入可以解决循环依赖.原理是:Spring控制了bean的生命周期,先实例化bean,后注入bean的属性.Spring中记录了正在创建中的bean(已经实例化但还没初始化完毕的bean),所以可以在注入属性时,从记录

  • Spring循环依赖的解决方案详解

    目录 简介 方案1. Feild注入单例(@AutoWired) 方案2. 构造器注入+@Lazy 方案3. Setter/Field注入单例 方案4. @PostConstruct 方案5. 实现ApplicationContextAware与InitializingBean 简介 说明 本文用实例介绍如何解决Spring的循环依赖问题. 相关网址 Spring循环依赖之问题复现详解 公共代码 package com.example.controller; import com.example

随机推荐