Spring6 当中的 Bean 循环依赖的具体处置惩罚方案+源码剖析 ...

打印 上一主题 下一主题

主题 870|帖子 870|积分 2610

1. Spring6 当中的 Bean 循环依赖的具体处置惩罚方案+源码剖析

@
目录

每博一文案
  1. 听完这段话就勇敢起来吧,在任何犹豫的时刻,一旦抱有人生就这么短短几十年,我不去做一定会后悔这样的想法,就会凭空多出几分勇气,比如:尝试新的穿衣风格,向喜欢的人表白,去特别贵的餐厅大吃一顿,对看不惯的人和事说不,不乐观的想,我们其实都是没有来路和归途的,能拥有的就是现在,所以想做什么就去做吧,冲动一点也没关系,吃点亏也没关系.
复制代码
1.1 Bean的循环依赖

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



  1. package com.rainbowsea.bean;
  2. public class Wife {
  3.     private String name;
  4.     private Husband husband;
  5.     public Wife() {
  6.     }
  7.     public Wife(String name, Husband husband) {
  8.         this.name = name;
  9.         this.husband = husband;
  10.     }
  11.     public String getName() {
  12.         return name;
  13.     }
  14.     public void setName(String name) {
  15.         this.name = name;
  16.     }
  17.     public Husband getHusband() {
  18.         return husband;
  19.     }
  20.     public void setHusband(Husband husband) {
  21.         this.husband = husband;
  22.     }
  23.     // toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
  24.     @Override
  25.     public String toString() {
  26.         return "Wife{" +
  27.                 "name='" + name + '\'' +
  28.                 ", husband=" + this.husband.getName() +
  29.                 '}';
  30.     }
  31. }
复制代码
Husband
  1. package com.rainbowsea.bean;
  2. public class Husband {
  3.     private String name;
  4.     private Wife wife;
  5.     public Husband() {
  6.     }
  7.     public Husband(String name, Wife wife) {
  8.         this.name = name;
  9.         this.wife = wife;
  10.     }
  11.     public String getName() {
  12.         return name;
  13.     }
  14.     public void setName(String name) {
  15.         this.name = name;
  16.     }
  17.     public Wife getWife() {
  18.         return wife;
  19.     }
  20.     public void setWife(Wife wife) {
  21.         this.wife = wife;
  22.     }
  23.     // toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误
  24.     @Override
  25.     public String toString() {
  26.         return "Husband{" +
  27.                 "name='" + name + '\'' +
  28.                 ", wife=" + this.wife.getName() +
  29.                 '}';
  30.     }
  31. }
复制代码
注意点: toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢堕落误。
1.2 singletion 下的 set 注入下的 Bean 的循环依赖

我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否可以或许解决?
预备工作:配置导入 相关的 spring 框架,让 Maven 帮我们导入 spring的相关jar包。
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5.     <modelVersion>4.0.0</modelVersion>
  6.     <groupId>com.rainbowsea</groupId>
  7.     <artifactId>spring6-007-circular-dependency-blog</artifactId>
  8.     <version>1.0-SNAPSHOT</version>
  9.     <packaging>jar</packaging>
  10.     <properties>
  11.         <maven.compiler.source>17</maven.compiler.source>
  12.         <maven.compiler.target>17</maven.compiler.target>
  13.     </properties>
  14.     <dependencies>
  15.         <dependency>
  16.             <groupId>org.springframework</groupId>
  17.             <artifactId>spring-context</artifactId>
  18.             <version>6.0.11</version>
  19.         </dependency>
  20.         
  21.         <dependency>
  22.             <groupId>junit</groupId>
  23.             <artifactId>junit</artifactId>
  24.             <version>4.13.2</version>
  25.             <scope>test</scope>
  26.         </dependency>
  27.     </dependencies>
  28. </project>
复制代码
配置相关的 spring.xml 文件信息。
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  5.    
  6.     <bean id="husbandBean"  scope="singleton">
  7.         <property name="name" value="小明" ></property>
  8.         <property name="wife" ref="wifeBean"></property>
  9.     </bean>
  10.    
  11.     <bean id="wifeBean"  scope="singleton">
  12.         <property name="name" value="小花"></property>
  13.         <property name="husband" ref="husbandBean"></property>
  14.     </bean>
  15. </beans>
复制代码
运行测试:

通过测试得知:在singleton + set注入的环境下,循环依赖是没有问题的。Spring可以解决这个问题。
  1. package com.rainbowsea.test;
  2. import com.rainbowsea.bean.Husband;
  3. import com.rainbowsea.bean.Wife;
  4. import org.junit.Test;
  5. import org.springframework.context.ApplicationContext;
  6. import org.springframework.context.support.ClassPathXmlApplicationContext;
  7. public class CircularDependencyTest {
  8.     @Test
  9.     public void testCircularDependency() {
  10.         ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
  11.         Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
  12.         System.out.println(husbandBean);
  13.         Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
  14.         System.out.println(wifeBean);
  15.     }
  16. }
复制代码
解决分析:
singleton + setter模式下可以解决的循环依赖问题
在singleton + setter 模式下,为什么循环依赖不会出现问题,Spring是怎样应对的?
主要缘故原由是:在这个 singleton 单例模式下,在Spring 容器中的 bean 对象是独一无二的对象,是唯一的一个。同志在该 singleton 单例模式下:Spring 对 Bean 的管理主要分为清晰的两个阶段

  • 第一个阶段:在Spring 容器加载的时候,实例Bean ,只要此中任意一个 Bean 实例化之后,马上举行一个“曝光” (注意:曝光不等于属性赋值,曝光了,但是属性并没有附上值的)
  • 第二个阶段:Bean “曝光”之后,再举行属性的赋值操作(调用 set()方法实现对属性的赋值操作)
核心解决方案是:实例化对象和对象的属性赋值分为两个阶段来完成,并不是一次性完成的。
简单来说:就是:singleton 优先被“曝光”,实例化和赋值是分开的,会优先把实例化的对象的地点曝光出来,因为在 singleton 单例模式下,bean 是唯一的一个,独一无二的,并且早晚都要举行赋值操作。提前曝光,背面再举行赋值也是无妨的。因为你弄来弄去,就是那唯一的一个 bean。不存在多个,不知道是哪一个的问题

1.3 prototype下的 set 注入下的 Bean 的循环依赖

我们再来测试一下:prototype+set注入的方式下,循环依赖会不会出现问题?
我们只需将 spring.xml 配置文件信息,修改为 protoype (多例)即可。

运行测试看看。

报错,报错信息如下:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'husbandBean' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'wifeBean' while setting bean property 'wife'
创建名为“husbandBean”的bean时堕落:请求的bean当前正在创建中:是否存在无法剖析的循环引用?通过测试得知,当循环依赖的所有Bean的scope="prototype"的时候,产生的循环依赖,Spring是无法解决的,会出现BeanCurrentlyInCreationException非常。
prototype下的 set 注入下的 Bean 的循环依赖;并不能解决循环依赖,缘故原由是:prototype 是多例的存在,多个 Bean 对象,不是唯一的一个Bean,无法确定是具体是哪个,Bean无法提前曝光。
  1. BeanCreationException 报错:当前的Bean正在处于创建中异常
复制代码
特别的:当两个bean的scope都是prototype的时候,才会出现非常,如果此中任意一个是singleton的,就不会出现非常了。是此中的任意一个 就行,就不会出现非常了。如果是三个 bean  的话,那就需要此中的任意两个 是为singleton才行。
缘故原由是:singleton 优先被“曝光”,实例化和赋值是分开的,会优先把实例化的对象的地点曝光出来,因为在 singleton 单例模式下,bean 是唯一的一个,独一无二的,并且早晚都要举行赋值操作。提前曝光,背面再举行赋值也是无妨的。因为你弄来弄去,就是那唯一的一个 bean。不存在多个,不知道是哪一个的问题。
测试:当两个bean的scope都是prototype的时候,才会出现非常,如果此中任意一个是singleton的,就不会出现非常了。
Husband 为 prototype ,Wife 为 singleten

反一下:Husband 为 singleten ,Wife 为 prototype

至于,三个 Bean ,需要任意两个为 singleten ,才不会报非常,就大家自行测试了。理论上就是:n 个 就需要 N-1个为 singleten 。
1.4 singleton下的构造注入产生的循环依赖

如果是基于构造注入(举行赋值),很明显,要调用构造方法举行赋值就一定要完完整整的举行一次性赋值+实例化,没有分段的,以是会产生循环依赖并且无法解决的,
以是编写代码时一定要注意。同样是报:  BeanCreationException 报错:当前的Bean正在处于创建中异常
我们来测试一下。

1.5 Spring 解决循环依赖的原理(源码剖析)

Spring 为什么可以解决 set+sigleton 模式下循环依赖呢?
根本缘故原由在于:这种方式可以做到将 “实例化 Bean” 和“给 Bean 属性赋值” 这两个动作分开去完成。实例化Bean的时候:调用无参数构造方法来完成此时可以先不给属性赋值(因为在 singleton 单例模式下,bean 是唯一的一个,独一无二的,并且早晚都要举行赋值操作。提前曝光,背面再举行赋值也是无妨的。因为你弄来弄去,就是那唯一的一个 bean),可以提前将Bean 对象“曝光”给外界
给Bean 属性赋值的时候:调用 setter()方法来完成(set注入完成,调用此中 bean对象当中的 set()方法,以是千万要记得写 set()方法)。
两个步骤是完全可以分离去完成的,并且这两步不要求在同一个时间点上完成。
也就是说,Bean 都是单例的,我们可以先把所有的单例 Bean 实例化出来,放到一个集合当中(我们可以将其称之为缓存),所有的单例 Bean 全部实例化完成之后,以后我们再逐步的调用 setter()方法给属性赋值,这样就解决了循环依赖的问题。
那么在 Spring 框架底层源码级别上是怎样实现的呢?如下:
我们先来分析一下:AbstractAutowireCapableBeanFactory类下的doCreateBean() 方法

doCreateBean() 方法 下调用的:addSingletonFactory() 方法,这里源码上使用了正则表达式,关于Lambda 表达式,由于设置的内容较多,想要了解更多的,大家可以移步至✏️✏️✏️ 函数式编程:Lambda 表达式_(ws, bs)>-CSDN博客
  1. addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
复制代码

下面这个DefaultSingletonBeanRegistry类,才是我们真正要探究的源码内容

在这个DefaultSingletonBeanRegistry 类当中中包罗三个紧张的属性同时也是三个Map集合:
  1.         /** Cache of singleton objects: bean name to bean instance. */
  2.         private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
  3.         /** Cache of singleton factories: bean name to ObjectFactory. */
  4.         private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
  5.         /** Cache of early singleton objects: bean name to bean instance. */
  6.         private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
复制代码
这三个缓存其实本质上是三个Map集合。

  • Cache of singleton objects: bean name to bean instance. 单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
  • Cache of early singleton objects: bean name to bean instance. 早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
  • Cache of singleton factories: bean name to ObjectFactory.单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】
  1. private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); 一级缓存
  2. private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); 二级缓存
  3. private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); 三级缓存
  4. 这个三个缓存都是Map集合
  5. Map集合的key 存储的都是bean的name(bean id)
  6. > 一级缓存存储的是:单例Bean对象,完整的单例Bean对象,也就是这个缓存中的Bean对象的属性都已经赋值了,是一个完整的Bean对象
  7. > 二级缓存存储的是: 早期的案例Bean对象,这个缓存中的单例Bean对象的属性灭有赋值,只是一个早期的实例对象
  8. > 三级缓存存储的是: 单例工厂对象,这个里面存储了大力的“工厂对象”,每一个单例Bean对象都会对应一个单例工厂对象。
  9. > 这个集合中存储的是,创建该单例对象时对应的那个单例工厂对象。   
复制代码
我们再来看,在该类中有这样一个方法 addSingletonFactory(),这个方法的作用是:将创建Bean对象的ObjectFactory对象提前曝光。这里我们Debug 调试看看。


再分析对应下面的源码:


从源码中可以看到:spring 会先从一级缓存中获取Bean 对象,如果获取不到,则再从二级缓存当中获取 Bean 对象,如果二级缓存照旧获取不到,则末了从三级缓存当中获取之前曝光的ObjectFactory 对象,通过ObjectFactory 对象获取到对应 Bean 实例,这样就解决了循环依赖的问题。
总结:
Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在剖析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当剖析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。
2. 总结:


  • Bean的循环依赖:A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
  • singletion 下的 set 注入下的 Bean 的循环依赖可以或许被解决。主要缘故原由是:在这个 singleton 单例模式下,在Spring 容器中的 bean 对象是独一无二的对象,是唯一的一个。同志在该 singleton 单例模式下:Spring 对 Bean 的管理主要分为清晰的两个阶段

    • 第一个阶段:在Spring 容器加载的时候,实例Bean ,只要此中任意一个 Bean 实例化之后,马上举行一个“曝光” (注意:曝光不等于属性赋值,曝光了,但是属性并没有附上值的)
    • 第二个阶段:Bean “曝光”之后,再举行属性的赋值操作(调用 set()方法实现对属性的赋值操作)
    核心解决方案是:实例化对象和对象的属性赋值分为两个阶段来完成,并不是一次性完成的。

  • prototype下的 set 注入下的 Bean 的循环依赖;并不能解决循环依赖,缘故原由是:prototype 是多例的存在,多个 Bean 对象,不是唯一的一个Bean,无法确定是具体是哪个,Bean无法提前曝光。
  • 特别的:当两个bean的scope都是prototype的时候,才会出现非常,如果此中任意一个是singleton的,就不会出现非常了。是此中的任意一个 就行,就不会出现非常了。如果是三个 bean  的话,那就需要此中的任意两个 是为singleton才行。

    • 至于,三个 Bean ,需要任意两个为 singleten ,才不会报非常,就大家自行测试了。理论上就是:n 个 就需要 N-1个为 singleten 。
    • 注意报错信息:org.springframework.beans.factory.BeanCreationException: 当前的Bean正在处于创建中非常

  • singleton下的构造注入产生的循环依赖;是基于构造注入(举行赋值),很明显,要调用构造方法举行赋值就一定要完完整整的举行一次性赋值+实例化,没有分段的,以是会产生循环依赖并且无法解决的,
  • Spring 解决循环依赖的原理(源码剖析):一级缓存,二级缓存,三级缓存的存在。提前“曝光”机制
3. 末了:

“在这个末了的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上汲取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的范畴奋斗。感谢你们,我们总会在某个时刻再次相遇。”


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

半亩花草

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表