(九)Spring之Bean的循环依赖问题
创始人
2024-02-01 00:19:26

文章目录

  • 环境
  • 什么是Bean的循环依赖
  • singleton下的set注入产生的循环依赖
  • prototype下的set注入产生的循环依赖
  • 构造注入产生的循环依赖
    • singleton下的构造注入
    • prototype下的构造注入
  • Spring解决循环依赖的机理(底层实现)

上一篇:(八)Bean的生命周期

环境

spring6里程碑版本的仓库
依赖:spring context依赖、junit依赖、log4j2依赖
log4j2.xml文件放到类路径下。

什么是Bean的循环依赖

A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。

singleton下的set注入产生的循环依赖

我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?
创建Bean:丈夫类Husband

/*** 丈夫类*/
public class Husband {private String name;private Wife wife;@Overridepublic String toString() {return "Husband{" +"name='" + name + '\'' +", wife=" + wife.getName() +'}';}public String getName() {return name;}public void setName(String name) {this.name = name;}public void setWife(Wife wife) {this.wife = wife;}
}

创建Bean:妻子类Wife

/*** 妻子类*/
public class Wife {private String name;private Husband husband;public void setName(String name) {this.name = name;}public void setHusband(Husband husband) {this.husband = husband;}public String getName() {return name;}@Overridepublic String toString() {return "Wife{" +"name='" + name + '\'' +", husband=" + husband.getName() +'}';}
}

创建spring.xml配置:

    

测试程序:

    @Testpublic void testSingletonSet(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);System.out.println(husbandBean);System.out.println(wifeBean);}

请添加图片描述
在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。、
怎么解决问题呢,先这样解释:(后面有底层解释,先这样理解)
Spring在解析xml文件的时候,遇见beanA,会先把beanA创建出来,然后发现beanA的属性需要beanB
然后Spring会去找beanB,发现beanB也是单例的,就会把对象先创建出来。
主要原因是,在这种模式下Spring对Bean的管理主要分为清晰的两个阶段:

  • 第一阶段:在Spring容器加载的时候,实例化Bean,只要其中任意一个Bean实例化后,就马上进行 ”曝光“ 【不等待属性赋值就曝光】
  • 第二阶段:Bean ”曝光“ 之后,再进行属性赋值。

核心的原因就是:实例化对象和对象的属性赋值分为两个阶段完成。
在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。

prototype下的set注入产生的循环依赖

我们再来测试一下:prototype+set注入的方式下,循环依赖会不会出现问题?
还是那两个夫妻Bean
然后创建一个新的spring配置文件:spring1.xml

    

测试程序:

    @Testpublic void testPrototypeSet(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring1.xml");Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);System.out.println(husbandBean);System.out.println(wifeBean);}

请添加图片描述
翻译为:创建名为“husbandBean”的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用?
会出现异常BeanCreationException,当前bean正处于创建中异常,
也就是说bean处于第二步(populateBean:填充属性)就卡住了
如果我们把其中一个Bean改为单例的试试

    

再次运行,发现运行正常。
请添加图片描述

构造注入产生的循环依赖

我们再来测试一下构造注入的方式下,spring是否能够解决这种循环依赖。
修改丈夫类:

/*** 丈夫类*/
public class Husband {private String name;private Wife wife;@Overridepublic String toString() {return "Husband{" +"name='" + name + '\'' +", wife=" + wife.getName() +'}';}public String getName() {return name;}public Husband(String name, Wife wife) {this.name = name;this.wife = wife;}
}

修改妻子类:

/*** 妻子类*/
public class Wife {private String name;private Husband husband;public Wife(String name, Husband husband) {this.name = name;this.husband = husband;}public String getName() {return name;}@Overridepublic String toString() {return "Wife{" +"name='" + name + '\'' +", husband=" + husband.getName() +'}';}
}

singleton下的构造注入

创建spring2.xml配置:

    

测试程序:

    @Testpublic void testSingletonConstructor(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");Husband husband = applicationContext.getBean("husband", Husband.class);Wife wife = applicationContext.getBean("wife", Wife.class);System.out.println(husband);System.out.println(wife);}

出现异常
请添加图片描述

prototype下的构造注入

修改spring2.xml:

    

再次运行测试程序,发现出现异常:
请添加图片描述

主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。

Spring解决循环依赖的机理(底层实现)

Spring的循环依赖的理论依据其实是基于Java的引用传递,当我们获取到对象的引用时,实例化Bean的时候:调用无参数构造方法来完成。对象的属性是可以延后设置的。
通过Bean的生命周期我们知道,Spring的单例对象完成初始化主要分为三步:

  • 第一步:实例化Bean,其实也就是调用对象的构造方法实例化对象
  • 第二步:Bean属性赋值 ,这一步主要是多bean的依赖属性进行填充
  • 第三步:Bean属性赋值 ,调用spring xml中的init 方法。

循环依赖主要发生在第一、第二步。
对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存)。
Spring为了解决单例的循环依赖问题,使用了三级缓存。
Spring在DefaultSingletonBeanRegistry这个类设置了三级缓存:

	private final Map singletonObjects = new ConcurrentHashMap(256);private final Map> singletonFactories = new HashMap(16);private final Map earlySingletonObjects = new ConcurrentHashMap(16);

这三个Map的key都是存储Bean的id,
这三级缓存分别指:

  • 三级缓存:singletonFactories :存储单例对象工厂,每一个单例Bean对象都会对应一个单例工厂对象。存储创建该单例对象时对应的那个单例工厂对象。
  • 二级缓存:earlySingletonObjects :存储提前曝光的单例Bean对象,这个Bean只是完成了初始化第一步。
  • 一级缓存:singletonObjects:存储完整的单例Bean对象,这个Bean完成了初始化1、2、3步了。

首先在创建BeanA对象的时候,会把BeanA对象的工厂存入(曝光)在三级缓存里面。

解决循环依赖底层的一个代码是DefaultSingletonBeanRegistry的getSingleton方法:

	protected Object getSingleton(String beanName, boolean allowEarlyReference) {Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {synchronized (this.singletonObjects) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {ObjectFactory singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}return (singletonObject != NULL_OBJECT ? singletonObject : null);}

Spring使用三个判断,分别从缓存的一级读取到三级:

  • 首先一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中。
  • 就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories获取
  • 就从三级缓存singletonFactory(三级缓存)获取

如果获取到了则:从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。

那么BeanA和BeanB的循环依赖完整的解决是:

     beanA首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象beanB此时就尝试去获取beanB,发现beanB还没有被创建,所以走beanB创建流程,beanB在初始化第一步的时候发现自己依赖了对象beanA,于是尝试获取beanA,尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全)尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于beanA通过ObjectFactory将自己提前曝光了,所以beanB能够通过ObjectFactory拿到beanA对象beanB拿到beanA对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回beanA中,beanA此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终beanA也完成了初始化,进去了一级缓存singletonObjects中,于是beanB拿到了beanA的对象引用,所以beanB的beanA对象完成了初始化。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
南美洲有哪些国家 南美洲一共有... 南美洲是南亚美利加洲的简称,位于西半球,南美洲大部分地区位于南半球,南美洲东临大西洋,西临太平洋,南...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
南美洲有哪些国家 南美洲一共有... 南美洲是南亚美利加洲的简称,位于西半球,南美洲大部分地区位于南半球,南美洲东临大西洋,西临太平洋,南...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...