Spring源码之 Bean 的循环依赖

简介: 循环依赖是 Spring 中经典问题之一,那么到底什么是循环依赖?简单说就是对象之间相互引用, 如下图所示:代码层面上很好理解,在 bean 创建过程中 class A 和 class B 又经历了怎样的过程呢?可以看出形成了一个闭环,如果想解决这个问题,那么在属性填充时要保证不二次创建 A对象 的步骤,也就是必须保证从容器中能够直接获取到 B。一、复现循环依赖问题Spring 中默认允许循环依赖的存在,但在 Spring Boot 2.6.x 版本开始默认禁用了循环依赖1. 基于xml复现循环依赖定义实体 Beanjava复制代码public class A {

循环依赖是 Spring 中经典问题之一,那么到底什么是循环依赖?简单说就是对象之间相互引用, 如下图所示:

代码层面上很好理解,在 bean 创建过程中 class A 和 class B 又经历了怎样的过程呢?

可以看出形成了一个闭环,如果想解决这个问题,那么在属性填充时要保证不二次创建 A对象 的步骤,也就是必须保证从容器中能够直接获取到 B。

一、复现循环依赖问题

Spring 中默认允许循环依赖的存在,但在 Spring Boot 2.6.x 版本开始默认禁用了循环依赖

1. 基于xml复现循环依赖

  • 定义实体 Bean
java复制代码public class A {
   private B b;
   public B getB() {
      return b;
   }
   public void setB(B b) {
      this.b = b;
   }
}
java复制代码public class B {
   private A a;
   public A getA() {
      return a;
   }
   public void setA(A a) {
      this.a = a;
   }
}
  • 重写 customizeBeanFactory 方法,禁止循环依赖
java复制代码public class NotAllowCircularXmlApplicationContext extends ClassPathXmlApplicationContext {
   public NotAllowCircularXmlApplicationContext(String... configLocations) throws BeansException {
      super(configLocations);
   }
   @Override
   protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
      // 禁止循环依赖
      super.setAllowCircularReferences(false);
      super.customizeBeanFactory(beanFactory);
   }
}
  • 配置 xml 文件
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
            http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd">
   <context:component-scan base-package="com.sff.demo.cycle"/>
   <bean id="a" class="com.sff.demo.cycle.A">
      <property name="b" ref="b"/>
   </bean>
    
    <bean id="b" class="com.sff.demo.cycle.B">
      <property name="a" ref="a"/>
   </bean>
</beans>
java复制代码public class TestAnnoMain {
   public static void main(String[] args) {
      testCycleRef();
   }
   public static void testXmlCycleRef() {
      NotAllowCircularXmlApplicationContext ac = new NotAllowCircularXmlApplicationContext("application-cycle.xml");
      A bean = ac.getBean(A.class);
      System.out.println(bean);
   }
}

运行结果:

2. 基于注解 @Autowired 复现循环依赖

  • 实体定义
java复制代码@Component
public class C {
   @Autowired
   private D d;
}
@Component
public class D {
   @Autowired
   private C c;
}
  • 重写 AnnotationConfigApplicationContext,禁止循环依赖
java复制代码public class NotAllowCircularAnnotationConfigApplicationContext extends AnnotationConfigApplicationContext {
   public NotAllowCircularAnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
      super();
      register(annotatedClasses);
      // 禁止循环依赖
      super.setAllowCircularReferences(false);
      refresh();
   }
}
  • 运行结果
java复制代码public class TestAnnoMain {
   public static void main(String[] args) {
      testCycleRef();
   }
   public static void testCycleRef() {
      NotAllowCircularAnnotationConfigApplicationContext ac = new NotAllowCircularAnnotationConfigApplicationContext(BeanConfig3.class);
      C bean = ac.getBean(C.class);
      System.out.println(bean);
   }
}

二、在 Spring 中是如何解决该问题的呢?

Spring 中利用三级缓存解决循环依赖问题,缓存定义在
DefaultSingletonBeanRegistry 中

java复制代码public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    // 一级缓存,保存 beanName 和 实例化、初始化好的完整 bean 对象
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    // 二级缓存,保存 beanName 和 未初始化的 bean 对象
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);  
    // 三级缓存,保存 beanName 和 lambda 表达式 () -> getEarlyBeanReference(beanName, mbd, bean)
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}

Spring 中利用三级缓存解决循环依赖的根本就是为了避免引用对象的循环创建,那么它是如何实现这一目的呢?

在本文案例中 class A 中 引用 class B ,同时在 class B 中引用 class A,在创建 A 对象时通过 addSingletonFactory方法,

java复制代码addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

在三级缓 singletonFactories.put(beanName,() -> getEarlyBeanReference(beanName, mbd, bean)) 中存放了 A 对象的这样一个 lambda 表达式,可以理解成回调函数。

当在创建 B 对象时,会调用通过 getSingleton 方法调用A对象三级缓存中的 lambda 表达式,也就是getEarlyBeanReference(beanName, mbd, bean) ,在没有AOP代理的情况下,可以直接返回 A 的对象信息,也就 getEarlyBeanReference(beanName, mbd, bean) 的参数 bean 就是提前缓存的 A 对象。

总结: 在 Spring 中 ,对象的创建分为实例化和初始化,实例化好但是未完成初始化的对象可以直接给其他对象引用,所以 Spring 中就把实例化好为初始化好的对象提前暴露出去,让其他对象能够进行引用,就完成了循环依赖的解耦!

作者:大飞学习笔记

链接:
https://juejin.cn/post/7261269922733359159

相关文章
|
7天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
23 2
|
23天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
7天前
|
缓存 架构师 Java
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
|
14天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
13天前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
39 9
|
1月前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
111 5
|
1月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
1月前
|
XML Java 数据格式
手动开发-简单的Spring基于注解配置的程序--源码解析
手动开发-简单的Spring基于注解配置的程序--源码解析
46 0
|
2月前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
193 24
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存