【Spring】Bean的循环依赖问题

简介: 【Spring】Bean的循环依赖问题

根据 【动力节点】最新Spring框架教程,全网首套Spring6教程,跟老杜从零学spring入门到高级 以及老杜的原版笔记 https://www.yuque.com/docs/share/866abad4-7106-45e7-afcd-245a733b073f?# 《Spring6》 进行整理, 文档密码:mg9b


Spring 相关文章整理汇总归纳于:https://www.yuque.com/u27599042/zuisie


什么是Bean的循环依赖

  • Bean的循环依赖,就是A对象中有B属性,B对象中有A属性。即我依赖你,你也依赖我。
  • 比如:丈夫类Husband,妻子类Wife,Husband中有Wife的引用,Wife中有Husband的引用。
package cw.spring.bean;
/**
 * ClassName: Wife
 * Package: cw.spring.bean
 * Description:
 *
 * @Author tcw
 * @Create 2023-05-12 16:35
 * @Version 1.0
 */
public class Wife {
    private String name;
    private Husband husband;
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setHusband(Husband husband) {
        this.husband = husband;
    }
    @Override
    public String toString() {
        return "Wife{" + "name='" + name + '\'' + ", husband=" + husband.getName() + '}';
    }
}
package cw.spring.bean;
/**
 * ClassName: Husband
 * Package: cw.spring.bean
 * Description:
 *
 * @Author tcw
 * @Create 2023-05-12 16:36
 * @Version 1.0
 */
public class Husband {
    private String name;
    private Wife wife;
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setWife(Wife wife) {
        this.wife = wife;
    }
    @Override
    public String toString() {
        return "Husband{" + "name='" + name + '\'' + ", wife=" + wife.getName() + '}';
    }
}

单例模式 + set 注入

单例模式 + set 注入情况下的循环依赖

  • 在单例模式 + set 注入的情况下,循环依赖是没有问题的,Spring 可以解决这个问题。
<bean id="husband" class="cw.spring.bean.Husband" scope="singleton">
  <property name="name" value="张三"/>
  <property name="wife" ref="wife"/>
</bean>
<bean id="wife" class="cw.spring.bean.Wife" scope="singleton">
  <property name="name" value="李四"/>
  <property name="husband" ref="husband"/>
</bean>
@org.junit.Test
public void test01() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Husband husband = applicationContext.getBean("husband", Husband.class);
    Wife wife = applicationContext.getBean("wife", Wife.class);
    System.out.println(husband);
    System.out.println(wife);
}

分析单例模式 + set 注入情况下的循环依赖

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

多例模式 + set 注入

<bean id="husband" class="cw.spring.bean.Husband" scope="prototype">
  <property name="name" value="张三"/>
  <property name="wife" ref="wife"/>
</bean>
<bean id="wife" class="cw.spring.bean.Wife" scope="prototype">
  <property name="name" value="李四"/>
  <property name="husband" ref="husband"/>
</bean>

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

单例模式 + 构造注入

  • 单例模式 + 构造注入的方式下产生的循环依赖是无法解决的
package cw.spring.pojo;
/**
 * ClassName: Husband
 * Package: cw.spring.pojo
 * Description:
 *
 * @Author tcw
 * @Create 2023-05-12 17:16
 * @Version 1.0
 */
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() + '}';
    }
}
package cw.spring.pojo;
import cw.spring.bean.Husband;
/**
 * ClassName: Wife
 * Package: cw.spring.pojo
 * Description:
 *
 * @Author tcw
 * @Create 2023-05-12 17:16
 * @Version 1.0
 */
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() + '}';
    }
}
<!-- 默认情况下,Spring创建对象都是单例模式的 -->
<bean id="wife" class="cw.spring.pojo.Wife">
  <constructor-arg name="name" value="张三"/>
  <constructor-arg name="husband" ref="husband"/>
</bean>
<bean id="husband" class="cw.spring.pojo.Husband">
  <constructor-arg name="name" value="李四"/>
  <constructor-arg name="wife" ref="wife"/>
</bean>
@org.junit.Test
public void test01() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    Husband husband = applicationContext.getBean("husband", Husband.class);
    Wife wife = applicationContext.getBean("wife", Wife.class);
    System.out.println(husband);
    System.out.println(wife);
}

  • Requested bean is currently in creation: Is there an unresolvable circular reference?
  • 请求的bean当前正在创建中:是否存在无法解决的循环引用?
  • 基于构造注入的方式下产生的循环依赖是无法解决的,使用构造注入无法创建完对象马上进行曝光,因为创建对象需要另一个对象作为属性,无法完成对象的创建(类似死锁)
  • 当创建A对象的时候,需要B对象,会马上进行B对象的创建,但是创建B对象又需要A对象,可是这个时候A对象还在创建中,导致B对象也无法完成创建,最终两个对象的创建都无法进行,使得创建对象和依赖注入失败
  • 基于构造注入的方式下产生的循环依赖是无法解决的,是由于构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。

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

  • Spring 可以解决 set + singleton 模式下循环依赖:
  • 根本的原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。
  • 实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。
  • 给Bean属性赋值的时候:调用setter方法来完成。
  • 两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。 也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。

源码跟踪

  • Spring 容器启动后,会执行 AbstractAutowireCapableBeanFactory 中的 doCreateBean 方法进行 Bean 的创建
  • 这里对Bean进行缓存,缓存的是创建Bean的工厂对象,从而对Bean进行曝光,使得需要使用该Bean的对象可以获取该Bean
  • addSingletonFactory 为 DefaultSingletonBeanRegistry(默认单例Bean注册) 类中的方法
  • 在 DefaultSingletonBeanRegistry 类中有三个Map集合,即缓存Bean的三级缓存
  • private final Map<String, Object> singletonObjects:一级缓存
  • private final Map<String, Object> earlySingletonObjects:二级缓存
  • private final Map<String, ObjectFactory<?>> singletonFactories:三级缓存
  • Map集合的key存储的都是bean的name(bean id):
  • 一级缓存存储的是:单例Bean对象。完整的单例Bean对象,也就是说这个缓存中的Bean对象的属性都已经赋值了。是一个完整的Bean对象。
  • 二级缓存存储的是:早期的单例Bean对象。这个缓存中的单例Bean对象的属性没有赋值。只是一个早期的实例对象。
  • 三级缓存存储的是:单例工厂对象。这个里面存储了大量的“工厂对象”,每一个单例Bean对象都会对应一个单例工厂对象。这个集合中存储的是,创建该单例对象时对应的那个单例工厂对象。
  • DefaultSingletonBeanRegistry 类中有 getSingleton 方法,用于获取单例 Bean 对象
  • 从源码中可以看到,spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。
  • 缓存 Bean 对象之后,会在 AbstractAutowireCapableBeanFactory 中的 doCreateBean 方法中对 Bean 对象进行赋值
  • 单例Bean实例对象的创建和赋值过程分开,提前进行Bean曝光,解决循环依赖问题
  • Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。
相关文章
|
8天前
|
缓存 架构师 Java
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
|
15天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
1月前
|
XML Java 数据格式
Spring从入门到入土(bean的一些子标签及注解的使用)
本文详细介绍了Spring框架中Bean的创建和使用,包括使用XML配置文件中的标签和注解来创建和管理Bean,以及如何通过构造器、Setter方法和属性注入来配置Bean。
70 9
Spring从入门到入土(bean的一些子标签及注解的使用)
|
1月前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
2月前
|
XML Java 数据格式
spring复习02,xml配置管理bean
详细讲解了Spring框架中基于XML配置文件管理bean的各种方式,包括获取bean、依赖注入、特殊值处理、属性赋值、集合类型处理、p命名空间、bean作用域及生命周期和自动装配。
spring复习02,xml配置管理bean
|
1月前
|
Java 开发者 Spring
Spring bean的生命周期详解!
本文详细解析Spring Bean的生命周期及其核心概念,并深入源码分析。Spring Bean是Spring框架的核心,由容器管理其生命周期。从实例化到销毁,共经历十个阶段,包括属性赋值、接口回调、初始化及销毁等。通过剖析`BeanFactory`、`ApplicationContext`等关键接口与类,帮助你深入了解Spring Bean的管理机制。希望本文能助你更好地掌握Spring Bean生命周期。
83 1
|
1月前
|
Java Spring
获取spring工厂中bean对象的两种方式
获取spring工厂中bean对象的两种方式
40 1
|
1月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
37 1
|
1月前
|
Java 开发者 Spring
Spring bean的生命周期详解!
本文详细介绍了Spring框架中的核心概念——Spring Bean的生命周期,包括实例化、属性赋值、接口回调、初始化、使用及销毁等10个阶段,并深入剖析了相关源码,如`BeanFactory`、`DefaultListableBeanFactory`和`BeanPostProcessor`等关键类与接口。通过理解这些核心组件,读者可以更好地掌握Spring Bean的管理和控制机制。
88 1
|
2月前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
40 4