一文详解Spring是怎样处理循环依赖的

 更新时间:2024年01月24日 11:06:47   作者:后端开发萌新  
循环依赖简单理解就是A,B 两个bean相互依赖,A依赖B,B依赖A,A->B、B->A大概就是这样,这篇文章主要介绍了Spring是怎样处理循环依赖的,文中通过代码示例给大家介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下

环境

Spring Framework Version: 5.3.x

Gradle Version:7.5.1

什么是循环依赖?

简单理解就是A,B 两个bean相互依赖,A依赖B,B依赖A

A->B、B->A大概就是这样.

所有注入场景的循环依赖Spring都能解决吗?

答案是 “不”, Spring不能够解决循环依赖的构造器注入,其它的注入方式都能解决

**注意:**能解决非构造器注入的循环依赖的前提是开启允许循环依赖(allowCircularReferences = true),在spring中默认开启,如果是在springboot2.x中,那么是默认关闭的

场景

我们来编写一段代码,模拟一下循环依赖场景

TestA.java

public class TestA {
	private TestB testB;

	public void setB(TestB testB) {
		this.testB = testB;
	}

	public void testCircularReference() {
		System.out.println("TestA bean register success...");
	}
}

TestB.java

public class TestB {
	private TestA testA;

	public void setA(TestA testA) {
		this.testA = testA;
	}

	public void testCircularReference() {
		System.out.println("TestB bean register success...");
	}
}

spring-context.xml

//使用的注入方式是自动装配,根据type自动装配
<bean id="testA" class="com.spring.demo.circularreference.TestA" autowire="byType"></bean>
<bean id="testB" class="com.spring.demo.circularreference.TestB" autowire="byType"></bean>

Main.java

public class Main {
	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");
		TestA testA = context.getBean(TestA.class);
		TestB testB = context.getBean(TestB.class);
		testA.testCircularReference();
		testB.testCircularReference();
    }
}

测试结果

a bean register success...
b bean register success...

好,代码编写完毕,测试也没问题,接下来我们来进行源码解析

Spring是怎么解决循环依赖的?

前置说明

Spring是通过三级缓存来解决循环依赖的,我们来看一张图片,认识一下三级缓存

在这里插入图片描述

一级缓存(singletonObjects):存放初始化完成的bean

二级缓存(earlySingletonObjects):存放实例化但未初始化的bean

三级缓存(singletonFactories):存放对象工厂,也就是把实例化但未初始化的bean包装为ObjectFactory

认识了三级缓存后,我们来剖析一下Spring是怎么利用三级缓存来解决循环依赖的

在实例化TestA之后,我们先会把TestA添加到三级缓存,然后进行属性填充,给TestB进行依赖注入,那么到TestB属性填充时,发现TestB也依赖TestA,这个时候去注入TestA,先从一级缓存获取,获取不到,再从二级缓存获取,二级缓存也没有,这个时候从三级缓存获取,获取到了,然后getObject()生成TestA对象(在这个时候,如果有代理,那么生成代理对象,如果没有代理,直接返回原本的对象),获取到TestA对象后移到二级缓存并返回,这个时候TestB就成功注入了TestA,然后TestB顺利的走完生命周期,就回到了第一个TestA,进行初始化,初始化完之后,再从二级缓存中取出再重新赋值(为了保证bean是同一个),最后添加到一级缓存中

这么说可能有点绕,我们结合一张图来看看

在这里插入图片描述

大概就是这样

  • 添加半成品的TestA(实例化但没进行属性注入与初始化)到三级缓存,然后注入TestB,在TestB中也要进行属性注入,然后就去注入TestA
  • 在TestB中注入TestA时,直接从缓存获取,因为这时三级缓存已经存在TestA,然后调用getEarlyBeanReference方法生成对象(如果有代理也是在此生成)
  • 添加到二级缓存中并删除三级缓存,最后返回,TestB属性注入完成,继续走Bean的生命周期
  • 回到原本TestA,这时TestB注入已经完成,然后初始化,最后从二级缓存中获取最新的bean,避免不是同一个对象(代理对象)

源码解析

addSingletonFactory

​ TestA在实例化之后添加三级缓存

在这里插入图片描述

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) 	{
            //添加三级缓存
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

getSingleton

TestB属性注入TestA过程中,从缓存获取TestA

在这里会获取到早期对象,移除三级缓存中的ObjectFactory

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 从一级缓存中获取TestA,这个时候是没有的
    Object singletonObject = this.singletonObjects.get(beanName);
    //isSingletonCurrentlyInCreation 在实例化之前就添加了创建标识,所以这里为true
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        //从二级缓存中获取TestA,这个时候二级缓存也是没有的
        singletonObject = this.earlySingletonObjects.get(beanName);
        //allowEarlyReference 是否允许循环依赖
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                //双重检查
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        //从三级缓存中获取ObjectFactory
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            //获取早期暴露对象(代理对象也是在此生成)
                            singletonObject = singletonFactory.getObject();
                            //添加到二级缓存中
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            //根据beanName从三级缓存中移除ObjectFactory
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

getEarlyBeanReference

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        //遍历smartInstantiationAware类型的后处理器
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            //获取早期bean引用
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

此时获取到的TestA是个代理对象,那么TestB里注入的是代理对象,然后TestA成功注入TestB,但是原本的TestA还是个普通的对象,怎么办呢?

很简单,之前不是将对象放到二级缓存了吗,所以在TestA注入完TestB并且初始化之后,这个时候会去二级缓存中获取最新的bean,并重新赋值,保证是同一个对象,看代码

//二级缓存提前暴露
if (earlySingletonExposure) {
    //从二级缓存中获取到最新的bean
    Object earlySingletonReference = getSingleton(beanName, false);
    //如果能获取到并且exposedObject和实例化之后的bean是保持一致的,那么就进行重新赋值
    if (earlySingletonReference != null) {
        if (exposedObject == bean) {
            exposedObject = earlySingletonReference;
        }
        else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            String[] dependentBeans = getDependentBeans(beanName);
            Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
            for (String dependentBean : dependentBeans) {
                if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                    actualDependentBeans.add(dependentBean);
                }
            }
            if (!actualDependentBeans.isEmpty()) {
                throw new BeanCurrentlyInCreationException(beanName,
                                "Bean with name '" + beanName + "' has been injected into other beans [" +
                                    StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                                    "] in its raw version as part of a circular reference, but has eventually been " +
                                  "wrapped. This means that said other beans do not use the final version of the " +
                                    "bean. This is often the result of over-eager type matching - consider using " +
                                    "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
            }
        }
    }
}

至此,循环依赖就解析完毕了,这里有个需要注意的点,就是生成代理对象那一块,需要依赖于SmartInstantiationAwareBeanPostProcessor生成代理的,是不能被处理的,例如@Async注解,它需要依赖于SmartInstantiationAwareBeanPostProcessor,被它代理的类,生成代理时机是初始化bean之后,那么如果在循环依赖里出现,例如TestA、TestB互相依赖,那么TestA使用了@Async注解,那么它的代理生成时机在bean的初始化之后,这样就会出现问题了,在TestB注入TestA时,从缓存中获取TestA,这时是没有被代理的,当原始的TestA注入完成后,在初始化之后生成代理,这个时候就会造成TestB里注入的TestA不是代理对象,而原始的TestA已经变成代理对象了,就会造成不是同一个对象

总结

看完源码之后,相信大家都有了一些了解,如果看完还是不太明白也没关系,自己跟着debug一遍然后做总结,加深印象,接下来我们对以上做一下总结吧。

如果被问到Spring是如何解决循环依赖的?

答: Spring是通过三级缓存去解决的循环依赖,具体来说就是在TestA实例化之后,属性填充之前,把Test包装成ObjectFactory对象并存入三级缓存中,这时注入TestB,然后在TestB里注入TestA时,就会从三级缓存里getObject,取出TestA半成品对象(如果是代理对象就进行创建),并且配合二级缓存,把它存入二级缓存中并在三级缓存中删除,最后回到原始TestA,在初始化原TestA之后,进行重新赋值,避免不是同一个对象。

为什么构造器注入不能解决循环依赖?

**答:**因为构造器注入是在实例化bean的时候,这时候三级缓存还没有添加,所以不能解决循环依赖。

为什么要设计三级缓存,一级、二级缓存行不行?

这个问题非常值得思考,不过不要陷入其中,大家可以思考一下

以上就是一文详解Spring是怎样处理循环依赖的的详细内容,更多关于Spring处理循环依赖的资料请关注脚本之家其它相关文章!

相关文章

  • 宝塔升级JDK版本超详细图文教程

    宝塔升级JDK版本超详细图文教程

    宝塔自动安装的JDK是一种用于开发和运行Java程序的软件开发工具包,下面这篇文章主要给大家介绍了关于宝塔升级JDK版本的相关资料,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-12-12
  • Spring MVC概念+项目创建+@RequestMappring案例代码

    Spring MVC概念+项目创建+@RequestMappring案例代码

    Spring MVC 是 Spring 提供的一个基于 MVC 设计模式的轻量级 Web 开发框架,本质上相当于 Servlet,这篇文章主要介绍了Spring MVC概念+项目创建+@RequestMappring,需要的朋友可以参考下
    2023-02-02
  • Spring Boot整合Bootstrap的超详细步骤

    Spring Boot整合Bootstrap的超详细步骤

    之前做前端开发,在使用bootstrap的时候都是去官网下载,然后放到项目中,在页面引用,下面这篇文章主要给大家介绍了关于Spring Boot整合Bootstrap的超详细步骤,需要的朋友可以参考下
    2023-05-05
  • spring整合redisson开启缓存方式

    spring整合redisson开启缓存方式

    这篇文章主要介绍了spring整合redisson开启缓存方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • Java Spring AOP之PointCut案例详解

    Java Spring AOP之PointCut案例详解

    这篇文章主要介绍了Java Spring AOP之PointCut案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-09-09
  • java实现根据ip地址获取地理位置

    java实现根据ip地址获取地理位置

    本文给大家汇总介绍了2种分别使用新浪和淘宝接口,实现根据IP地址获取详细的地理位置的代码,非常的实用,有需要的小伙伴可以参考下。
    2016-03-03
  • 一文详解Java中流程控制语句

    一文详解Java中流程控制语句

    在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的。也就是说,程序的流程对运行结果有直接的影响。所以,我们必须清楚每条语句的执行流程。本文就来通过一些示例带大家详细了解一下
    2022-10-10
  • Java中finally关键字对返回值的影响详解

    Java中finally关键字对返回值的影响详解

    这篇文章主要介绍了Java中finally关键字对返回值的影响详解,执行完try catch里面内容准备return时,如果还有finally需要执行这是编译器会为我们增加一个全局变量去暂存return 的值,等到finally执行完成去return这个全局变量,需要的朋友可以参考下
    2024-01-01
  • 一篇文章带你初步认识Maven

    一篇文章带你初步认识Maven

    这篇文章主要为大家初步认识了Maven,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • spring profile 多环境配置管理详解

    spring profile 多环境配置管理详解

    这篇文章主要介绍了 spring profile 多环境配置管理详解的相关资料,需要的朋友可以参考下
    2017-01-01

最新评论