Spring 如何处理循环依赖?

文章目录

  • Spring 如何处理循环依赖?
    • 项目环境
    • 1.什么是循环依赖?
    • 2.Spring 如何来处理循环依赖?
      • 2.1 allowCircularReferences 参数分析
      • 2.2 doCreateBean 方法分析
      • 2.3 依赖注入阶段
    • 3.总结
    • 4.两个问题补充
    • 5.参考

项目环境

  • jdk 1.8
  • spring 5.2.2.RELEASE
  • github 地址:https://github.com/huajiexiewenfeng/thinking-in-spring
    • 本章模块:questions

1.什么是循环依赖?

我们先来看一个示例

Student 学生类,学生有一个教室的字段属性,表示学生在哪个教室

public class Student {private Long id;private String name;@Autowiredprivate ClassRoom classRoom;...

ClassRoom 教室类,同样教室里面又包含学生的信息,这里我们用集合表示

public class ClassRoom {private Long id;private String name;@Autowiredprivate Collection<Student> students;...

引用(依赖)关系如下,本例中我们采用 @Autowired 方法注入:

这就是循环引用(依赖),形成了下面这么一个环:

Student -> @Autowired ClassRoom -> @Autowired Collection<Student> -> Student

测试类

public class CircularReferencesDemo {public static void main(String[] args) {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();// 注册applicationContext.register(CircularReferencesDemo.class);// 循环引用默认开启applicationContext.setAllowCircularReferences(true);// 开启applicationContext.refresh();Student student = applicationContext.getBean(Student.class);ClassRoom classRoom = applicationContext.getBean(ClassRoom.class);System.out.println("student:" + student);System.out.println("classRoom:" + classRoom);// 关闭applicationContext.close();}@Beanpublic Student student() {Student student = new Student();student.setId(1L);student.setName("小仙");return student;}@Beanpublic ClassRoom classRoom() {ClassRoom classRoom = new ClassRoom();classRoom.setId(1L);classRoom.setName("教室1");return classRoom;}}

执行结果:

student:Student{id=1, name='小仙', classRoom.name =教室1}
classRoom:ClassRoom{id=1, name='教室1', students=[Student{id=1, name='小仙', classRoom.name =教室1}]}

可以看到 spring 默认是支持循环引用的,applicationContext.setAllowCircularReferences(true); 如果不设置的话是默认为 true。

如果设置为 false,会抛出 org.springframework.beans.factory.UnsatisfiedDependencyException 异常。

2.Spring 如何来处理循环依赖?

2.1 allowCircularReferences 参数分析

这里我们可以从 setAllowCircularReferences 方法设置的参数为切入点来分析源码。

参数源码位置:AbstractAutowireCapableBeanFactory#allowCircularReferences

通过 IDEA 可以找到哪些源码应用到了这个参数

源码位置如下:AbstractAutowireCapableBeanFactory#doCreateBean

如果学习了 Spring 系列其他文章,这个方法就比较熟悉了,我们来做简单的回顾

  • AbstractApplicationContext#refresh //Spring 应用上下文启动 ;
  • AbstractApplicationContext#finishBeanFactoryInitialization // 在 BeanFactory 初始化完成阶段,通过 beanDefinitionNames 来遍历我们所有的 BeanDefintion,逐一进行 getBean(beanName) 操作,通过我们的 BeanDefinition 创建 bean 对象,并缓存到 DefaultSingletonBeanRegistry#singletonObjects 中;
  • getBean 的调用链路 getBean -> doGetBean -> createBean -> doCreateBean。

最终会调用到我们这个 doCreateBean 方法来创建 Bean 的实例。

2.2 doCreateBean 方法分析

doCreateBean 方法中和循环依赖相关的源码如下:

     // 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");}addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}

可以看到源码中的注释已经做了说明,翻译过来就是:

早期缓存的 singletons 是来解决循环引用,即使被 BeanFactoryAware 这样的生命周期接口触发

可以看出这个设计就是来解决循环引用问题的。

打上断点并设置条件,启动 DeBug 进行调试

beanName.equals("student")||beanName.equals("classRoom")


继续往下探,进入到以下代码

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

这行代码主要做两件事

  • 第一是根据 BeanName 和 BeanDefinition 的元信息来获取 EarlyBeanReference 早期 Bean 的引用

源代码如下:

注释的意思是说创建一个特殊的 bean 为了早期访问,用来解决循环引用问题。

 /*** Obtain a reference for early access to the specified bean,* typically for the purpose of resolving a circular reference.* @param beanName the name of the bean (for error handling purposes)* @param mbd the merged bean definition for the bean* @param bean the raw bean instance* @return the object to expose as bean reference*/protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (BeanPostProcessor bp : getBeanPostProcessors()) {if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);}}}return exposedObject;}
  • 第二件事 addSingletonFactory,放在当前的场景来分析就是,当 Student 类创建 bean 实例的过程中,在 populateBean 属性赋值阶段前,将这个早期的特殊的 bean 放到 singletonFactories 集合中,对象每次进来会缓存一个对象代码如下:
 protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {if (!this.singletonObjects.containsKey(beanName)) {this.singletonFactories.put(beanName, singletonFactory);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}}

核心代码:this.singletonFactories.put(beanName, singletonFactory);

2.3 依赖注入阶段

继续往下进入到 populateBean 属性赋值阶段,同样的我们再打算两个断点,分别是 Spring Bean 属性赋值阶段和 Spring Bean 初始化阶段

在 populateBean 属性赋值阶段,由于 Student 类的属性 classRoom 是 @Autowired ,会触发依赖注入 doResolveDependency 方法(这个方法在前面的文章 第九章:IoC 依赖注入(专题)-下 7.依赖处理过程 小节 分析过源码),所以又会进入到 doCreateBean 方法来先创建这个 ClassRoom 类的实例对象。

此时的 singletonFactories 中存放了两个早期的特殊的 bean,如下图所示:

那么按照分析我们继续往下到 ClassRoom 的 populateBean 阶段,此时由于 ClassRoom 类的属性 students 也是是 @Autowired,所以我们又会进入到 doResolveDependency 方法中,但是此时的 singletonFactories 包含了 student 的早期对象,所以后续的过程有区别,可以猜想到 Spring 可能会将这个早期的 student 设置到 ClassRoom 类的属性中。

下面我们继续来看 doResolveDependency 调用链路:

doResolveDependency -> resolveMultipleBeans -> findAutowireCandidates -> getBean -> getSingleton

getSingleton 代码如下:

从上面的调试截图可以看到,首先根据 beanName 获取 student 相关的 singletonFactory,调用 singletonFactory.getObject() 获取 singletonObject 对象,同时将这个早期的 student 对象放到 earlySingletonObjects 集合中,并且从 singletonFactories 移出该对象的 Factory 创建方法。

最后返回的这个 singletonObject 对象就是早期的 student 对象,然后将这个早期的 student 对象设置到属性中,具体的 set 方法在 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject 中 666 行,相关代码如下:

         if (value != null) {ReflectionUtils.makeAccessible(field);field.set(bean, value);}

我们直接看最后的结果如下图所示:

同样的逻辑,Student 的 classRoom 属性也会经过上面的过程,再次进入到 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject 中 666 行,结果如下图所示:

可以看到 Student 的 classRoom 属性也被设置为 ClassRoom 的实例对象,这样 Student 和 ClassRoom 就完成了循环依赖的过程。

3.总结

Spring 解决循环依赖的方法可以总结为下面几点:

  • 首先,Spring 提供 setAllowCircularReferences 方法可以设置是否开启循环依赖,默认是允许循环依赖。

  • 其次,Spring 设计了三个集合来处理循环依赖,分别是 singletonFactories,earlySingletonObjects,registeredSingletons

    • singletonFactories 用来缓存 Bean 创建的 Lamdba 表达式创建方法;
    • earlySingletonObjects 用来缓存早期的 bean 的引用;
    • registeredSingletons 记录已经注册的 bean。
  • 最后,当一个类 A 通过字段注入的方式依赖另一个类 B 的时候,如果 IoC 容器也没有 B 类的缓存,核心方法 DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

    • 先从 singletonFactories 获取到当前 bean 的实例创建方法;
    • 再通过 getObject 方法获取到早期的 bean 的实例引用;
    • 最后将这个 bean 的实例设置当前的字段属性中。

4.两个问题补充

  • IoC 容器中还没有 bean 的实例,那么通过 beanName 如何获取 bean 的实例?

    • 虽然没有 bean 的实例,但是有 BeanDefinition 元信息,通过 beanName 获取 BeanDefinition 元信息,然后实例化 Bean。
  • doCreateBean 完成之后又做了什么?

    • 创建完 bean 之后,调用了 getSingleton 方法,在这个方法的最后又调用了 DefaultSingletonBeanRegistry#addSingleton 方法,将创建好的 bean 设置到 singletonObjects 中,而这个 singletonObjects 集合对象就是 IoC 容器所有 Bean 实例(Singleton)的缓存集合。

5.参考

  • 极客时间-小马哥《小马哥讲Spring核心编程思想》

Spring 如何处理循环依赖?相关推荐

  1. 被问麻了,Spring 如何处理循环依赖?

    点击关注公众号,利用碎片时间学习 前言 Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题,在前面Bean实例化流程中,对属性注入一文多多少少对循环依赖有过介绍,这篇文章详细讲一下Spr ...

  2. Spring源码剖析-Spring如何处理循环依赖

    前言 你是不是被这个骚气的标题吸引进来的,_ 喜欢我的文章的话就给个好评吧,你的肯定是我坚持写作最大的动力,来吧兄弟们,给我一点动力 Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题, ...

  3. 图解Spring解决循环依赖

    点击上方蓝色"方志朋",选择"设为星标"回复"666"获取独家整理的学习资料! 来源:juejin.cn/post/684490412216 ...

  4. spring 循环依赖_简单说说 Spring 的循环依赖

    作者 | 田伟然 回首向来萧瑟处,归去,也无风雨也无晴. 杏仁工程师,关注编码和诗词. 前言 本文最耗时间的点就在于想一个好的标题, 既要灿烂夺目,又要光华内敛,事实证明这比砍需求还要难! 由于对象之 ...

  5. Spring 的循环依赖:真的必须非要三级缓存吗?

    作者:青石路 www.cnblogs.com/youzhibing/p/14337244.html 写作背景 做 Java 开发的,一般都绕不开 Spring,那么面试中肯定会被问到 Spring 的 ...

  6. 【源码分析】Spring的循环依赖(setter注入、构造器注入、多例、AOP)

    写在前面 首先最简单的循环依赖demo就是:A->B 且 B->A.本文围绕这个例子去讲解setter注入的循环依赖.构造器注入循环依赖.多例的循环依赖.带AOP的循环依赖.以下是一些结论 ...

  7. Spring如何处理循环引用

    Spring如何处理循环引用 一,划重点 Spring处理循环依赖记录,要是写错了,喷轻一点. 解题核心: 1.AbstractBeanFactory的2个getSingleton方法 2.early ...

  8. Spring当中循环依赖很少有人讲,今天让我们来看看吧

    网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...

  9. Spring当中循环依赖很少有人讲,今天一起来学习!

    网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...

最新文章

  1. 【风控系统】风控中心—京东基于Spark的风控系统架构实践和技术细节
  2. JVM内存模型和类加载运行机制
  3. 语言中的petchar运用_自闭症儿童语言障碍家庭训练,需要融入这些方法
  4. 1-springboot基础
  5. Mysql 中的SSL 连接
  6. Discuz!NT3.0博客扩展
  7. Atitit 外观ui调整法 表单与表格列表
  8. SQL Server2005彻底卸载
  9. excel怎么设置打印区域_淘宝卖家想要打印快递单怎么设置
  10. MATLAB图像形状识别
  11. 抖音视频怎么下载MP4格式怎么转换为MP3
  12. 如何使用python将数据写入txt文件
  13. 谁是史上最强-用爬虫分析IMDB TOP250电影数据
  14. Illegal character: U+00A0
  15. USYD悉尼大学DATA1002 详细作业解析Module5
  16. Linux配置自动获取ip方式和静态ip方
  17. 【R语言】ggplot2作图补充(1)
  18. 阿里云OSS上传下载跨域问题
  19. ug12在win8计算机名错,Win8安装UG9.0时出错提示“UGII_TMP_DIR 被设为一个有无效(非ASCII)字符的目录”怎么办...
  20. Ubuntu16.04搜狗拼音输入法候选栏无法显示中文(英文乱码)

热门文章

  1. CPU锁频率在0.78 GHz
  2. 实验六:视图及数据库系统安全
  3. ResNet50模型识别二维化的心电信号——以MIT-BIH心律失常数据库为例
  4. 基于多目标算法的冷热电联供型综合能源系统运行优化 粒子群算法 平台:MATLAB
  5. 2022全球智博会如约将至 新设工业视觉重磅展区
  6. 2台博能传动伺服驱动器使用105报文(DSC)实现绝对齿轮同步
  7. Linux常见查看命令
  8. 使用PHPMailer实现PHP通过QQ邮箱发邮件功能
  9. APP使用相机CameraX
  10. Python多用户在线聊天系统(Socket网络编程),控制台输出版