0%

Bean的循环依赖问题

  • Bean的循坏依赖问题
    • 单例模式+set注入
    • 多例模式+set注入(报异常)
    • 单例模式+构造注入(报异常)
    • Spring解决循坏依赖的机理(源码分析)

九、Bean的循坏依赖问题

9.1 什么是Bean的循坏依赖

  • Bean 的循环依赖,就是指 A 对象中有 B 属性,B 对象中有 A 属性。

    • 我依赖你,你也依赖我

    比如:丈夫类 Husband,妻子类 WifeHusband 中有 Wife 的引用,Wife 中有 Husband 的引用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package com.f.spring6.bean;

    /**
    * @author fzy
    * @date 2024/1/21 16:00
    */
    public class Husband {
    private String name;
    private Wife wife;

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public void setWife(Wife wife) {
    this.wife = wife;
    }

    @Override
    public String toString() {
    return "Husband{" +
    "name='" + name + '\'' +
    ", wife=" + wife.getName() +
    '}';
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package com.f.spring6.bean;

    /**
    * @author fzy
    * @date 2024/1/21 16:00
    */
    public class Wife {
    private String name;
    private Husband husband;

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public void setHusband(Husband husband) {
    this.husband = husband;
    }

    @Override
    public String toString() {
    return "Wife{" +
    "name='" + name + '\'' +
    ", husband=" + husband.getName() +
    '}';
    }
    }

9.2 单例模式+set注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--单例模式+set注入下的循坏依赖-->
<bean id="husband" class="com.f.spring6.bean.Husband">
<property name="name" value="jack"/>
<property name="wife" ref="wife"/>
</bean>
<bean id="wife" class="com.f.spring6.bean.Wife">
<property name="name" value="lucy"/>
<property name="husband" ref="husband"/>
</bean>
</beans>
1
2
3
4
5
6
7
8
@Test
public void testCD() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husband = applicationContext.getBean("husband", Husband.class);
System.out.println(husband);
Wife wife = applicationContext.getBean("wife", Wife.class);
System.out.println(wife);
}
  • 分析 “单例模式+set注入” 情况下的循坏依赖:

    • set + singleton 模式下 Spring 对 Bean 的管理主要分为两个阶段:
      • 第一个阶段:在 Spring 容器加载的时候,实例化 Bean只要其中任意一个 Bean 实例化之后,马上进行“曝光”,不等属性赋值就 “曝光”
        • 提前曝光,不等属性赋值就可以使用,因为单例下就只会创建该类型的对象一个,早曝光和晚曝光一样,都是这个对象。
      • 第二个阶段:Bean “曝光” 之后,再进行属性的赋值,调用 set 方法。
    • 解决单例模式 + set 注入情况下的循环依赖的核心解决方案是:实例化对象和对象的属性赋值分为两个阶段来完成的

    注意:只有在 scopesingleton 的情况下,Bean 才会采取提前 “曝光” 的措施prototype 下不会进行提前曝光,如果多例模式下多个对象进行提前曝光,则 Spring 在使用对象的时候,不知道要使用哪个对象。

9.3 多例模式+set注入(报异常)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--多例模式+set注入下的循环依赖-->
<!--存在问题,会出现异常BeanCurrentlyInCreationException-->
<bean id="husband" class="com.f.spring6.bean.Husband" scope="prototype">
<property name="name" value="jack"/>
<property name="wife" ref="wife"/>
</bean>
<bean id="wife" class="com.f.spring6.bean.Wife" scope="prototype">
<property name="name" value="lucy"/>
<property name="husband" ref="husband"/>
</bean>
</beans>
1
2
3
4
5
6
7
8
@Test
public void testCD() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husband = applicationContext.getBean("husband", Husband.class);
System.out.println(husband);
Wife wife = applicationContext.getBean("wife", Wife.class);
System.out.println(wife);
}
  • 分析 “多例模式+set注入” 情况下的循坏依赖:

    • 当循环依赖的两个 beanscope 都是 prototype 的时候,即在 prototype + set 注入下的循环依赖,会存在问题,会出现异常 BeanCurrentlyInCreationException(当前的 Bean 正处于创建中异常)。

      1
      org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'husband' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'wife' while setting bean property 'wife'

    原因在于:

    • 因为都是多例模式,所以每次需要对象的时候都会 new 新的对象出来,A 对象中需要 B 对象,会 new 一个新的 B 对象,B 对象中需要 A 对象,会 new 一个新的 A 对象,新的 A 对象中需要 B 对象,会再 new 一个新的 B 对象……最终导致对象永远创建不完。
      • 如果其中任意一个是 singleton 的,就不会出现异常(因为单例模式下的 bean 实例化完成以后就会进行“曝光”,“曝光”后就可以停止死循环依赖)。
  • singleton 表示在解析配置文件时就会被创建,当该 singleton Bean 中的属性需要 prototype Bean 时,可以马上创建一个 prototype 对象赋值给 singleton,而 prototype 对象所需的 singleton 只有唯一的一个,从而可以让依赖停止下来,所以没有问题。

9.4 单例模式+构造注入(报异常)

  • 单例模式 + 构造注入的方式下产生的循环依赖是无法解决的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.f.spring6.bean2;

    /**
    * @author fzy
    * @date 2024/1/21 16:00
    */
    public class Husband {
    private String name;
    private Wife wife;

    public Husband(String name, Wife wife) {
    this.name = name;
    this.wife = wife;
    }

    public String getName() {
    return name;
    }

    @Override
    public String toString() {
    return "Husband{" +
    "name='" + name + '\'' +
    ", wife=" + wife.getName() +
    '}';
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.f.spring6.bean2;

    /**
    * @author fzy
    * @date 2024/1/21 16:00
    */
    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;
    }

    @Override
    public String toString() {
    return "Wife{" +
    "name='" + name + '\'' +
    ", husband=" + husband.getName() +
    '}';
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--单例模式+构造注入下的循坏依赖-->
    <bean id="husband" class="com.f.spring6.bean2.Husband">
    <constructor-arg name="name" value="jack"/>
    <constructor-arg name="wife" ref="wife"/>
    </bean>
    <bean id="wife" class="com.f.spring6.bean2.Wife">
    <constructor-arg name="name" value="lucy"/>
    <constructor-arg name="husband" ref="husband"/>
    </bean>
    </beans>
    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void testCD2() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
    Husband husband = applicationContext.getBean("husband", Husband.class);
    System.out.println(husband);
    Wife wife = applicationContext.getBean("wife", Wife.class);
    System.out.println(wife);
    }
    • 会报异常:

      1
      org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'husband' defined in class path resource [spring2.xml]: Cannot resolve reference to bean 'wife' while setting constructor argument
    • 基于构造注入方式下的循环依赖是会报异常的,因为创建对象需要另一个对象作为属性,导致无法完成对象的创建。

      例如:当创建 A 对象的时候,需要 B 对象,会马上进行 B 对象的创建,但是创建 B 对象又需要 A 对象,可是这个时候 A 对象还在创建中,导致 B 对象也无法完成创建,最终两个对象的创建都无法进行,使得创建对象和依赖注入失败。

      • 基于构造注入的方式下产生的循环依赖问题是无法解决的,因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程不能分离

9.5 Spring解决循坏依赖的机理(源码分析)

  • Spring 可以解决 set + singleton 模式下循环依赖:

    • 根本原因在于:这种方式可以做到将 “实例化Bean”“给Bean属性赋值” 这两个动作分开去完成。

      • 实例化 Bean 的时候:调用无参数构造方法来完成,此时可以先不给属性赋值,可以提前将该 Bean 对象“曝光”给外界
      • Bean 属性赋值的时候:调用 setter 方法来完成。

      两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。

      也就是说,Bean 都是单例的,我们可以先把所有的单例 Bean 实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例 Bean 全部实例化完成之后,我们再慢慢的调用 setter 方法给单例 Bean 的属性赋值。这样就解决了循环依赖的问题。

  • 我们来看 Spring 的源码:

    • Spring 容器启动后,会执行 AbstractAutowireCapableBeanFactory 中的 doCreateBean 方法进行 Bean 的创建:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
      throws BeanCreationException {

      // Instantiate the bean.
      BeanWrapper instanceWrapper = null;
      if (mbd.isSingleton()) {
      instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
      }
      if (instanceWrapper == null) {
      // 实例化Bean,但还没有给Bean的属性赋值
      instanceWrapper = createBeanInstance(beanName, mbd, args);
      }
      Object bean = instanceWrapper.getWrappedInstance();
      Class<?> beanType = instanceWrapper.getWrappedClass();
      if (beanType != NullBean.class) {
      mbd.resolvedTargetType = beanType;
      }
      ......
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      ......
      // Eagerly cache singletons to be able to resolve circular references
      // even when triggered by lifecycle interfaces like BeanFactoryAware.
      boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
      isSingletonCurrentlyInCreation(beanName));
      if (earlySingletonExposure) {
      if (logger.isTraceEnabled()) {
      logger.trace("Eagerly caching bean '" + beanName +
      "' to allow for resolving potential circular references");
      }
      // 缓存单例Bean
      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
      }
      ......
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
      Assert.notNull(singletonFactory, "Singleton factory must not be null");
      synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
      // 将单例Bean对应的单例工厂对象存储到三级缓存中
      this.singletonFactories.put(beanName, singletonFactory);
      this.earlySingletonObjects.remove(beanName);
      this.registeredSingletons.add(beanName);
      }
      }
      }

      这里对 Bean 进行缓存,缓存的是创建 Bean 的工厂对象,从而对 Bean 进行曝光,使得需要使用该 Bean 的对象可以获取该 Bean

    • addSingletonFactoryDefaultSingletonBeanRegistry(默认单例 Bean 注册) 类中的方法。

      • DefaultSingletonBeanRegistry 类中有三个比较重要的 Map 集合,即缓存 Bean 的三个缓存:

        • private final Map<String, Object> singletonObjects:一级缓存
        • private final Map<String, Object> earlySingletonObjects:二级缓存
        • private final Map<String, ObjectFactory<?>> singletonFactories:三级缓存

        Map 集合的 key 存储的是 beannamebean id):

        • 一级缓存存储的是:单例 Bean 对象,完整的单例 Bean 对象。也就是说这个缓存中的 Bean 对象的属性都已经赋值了,是一个完整的 Bean 对象。
        • 二级缓存存储的是:早期的单例 Bean 对象。这个缓存中的单例 Bean 对象的属性没有赋值,只是一个早期的实例对象。
        • 三级缓存存储的是:单例工厂对象。这个里面存储了大量的“工厂对象”,每一个单例 Bean 对象都会对应一个单例工厂对象。这个集合中存储的是,创建该单例对象时对应的那个单例工厂对象。
    • @Nullable
      protected Object getSingleton(String beanName, boolean allowEarlyReference) {
          // Quick check for existing instance without full singleton lock
          Object singletonObject = this.singletonObjects.get(beanName);
          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
                      // 从一级缓存中获取单例Bean对象
                      singletonObject = this.singletonObjects.get(beanName);
                      if (singletonObject == null) {
                          // 从二级缓存中获取单例Bean对象
                          singletonObject = this.earlySingletonObjects.get(beanName);
                          if (singletonObject == null) {
                              ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                              if (singletonFactory != null) {
                                  // 从三级缓存中获取单例Bean对象
                                  singletonObject = singletonFactory.getObject();
                                  this.earlySingletonObjects.put(beanName, singletonObject);
                                  this.singletonFactories.remove(beanName);
                              }
                          }
                      }
                  }
              }
          }
          return singletonObject;
      }
      

      从源码中可以看到,Spring 会先从一级缓存中获取 Bean

      如果获取不到,则从二级缓存中获取 Bean

      如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的 ObjectFactory 对象,通过 ObjectFactory 对象获取 Bean 实例,这样就解决了循环依赖的问题。

    • 单例 Bean 对象的创建和赋值过程分开,提前进行 Bean 曝光,从而解决循环依赖问题。

  • Spring 只能解决 setter 方法注入的单例 bean 之间的循环依赖。

    ClassA 依赖 ClassBClassB 又依赖 ClassA,形成循环依赖。

    Spring 在创建 ClassA 对象后,不需要等待给属性赋值,就直接将其曝光到 bean 缓存当中。在解析 ClassA 的属性时,发现其依赖于 ClassB,就去获取 ClassB,当解析 ClassB 的属性时,又发现其依赖于 ClassA ,但此时 ClassA 已经被提前曝光,加入了 bean 的缓存中,所以直接从缓存中获取 ClassA 实例即可,从而解决循环依赖问题。

---------------The End---------------