Spring 如何处理循环依赖?
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 如何处理循环依赖?相关推荐
- 被问麻了,Spring 如何处理循环依赖?
点击关注公众号,利用碎片时间学习 前言 Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题,在前面Bean实例化流程中,对属性注入一文多多少少对循环依赖有过介绍,这篇文章详细讲一下Spr ...
- Spring源码剖析-Spring如何处理循环依赖
前言 你是不是被这个骚气的标题吸引进来的,_ 喜欢我的文章的话就给个好评吧,你的肯定是我坚持写作最大的动力,来吧兄弟们,给我一点动力 Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题, ...
- 图解Spring解决循环依赖
点击上方蓝色"方志朋",选择"设为星标"回复"666"获取独家整理的学习资料! 来源:juejin.cn/post/684490412216 ...
- spring 循环依赖_简单说说 Spring 的循环依赖
作者 | 田伟然 回首向来萧瑟处,归去,也无风雨也无晴. 杏仁工程师,关注编码和诗词. 前言 本文最耗时间的点就在于想一个好的标题, 既要灿烂夺目,又要光华内敛,事实证明这比砍需求还要难! 由于对象之 ...
- Spring 的循环依赖:真的必须非要三级缓存吗?
作者:青石路 www.cnblogs.com/youzhibing/p/14337244.html 写作背景 做 Java 开发的,一般都绕不开 Spring,那么面试中肯定会被问到 Spring 的 ...
- 【源码分析】Spring的循环依赖(setter注入、构造器注入、多例、AOP)
写在前面 首先最简单的循环依赖demo就是:A->B 且 B->A.本文围绕这个例子去讲解setter注入的循环依赖.构造器注入循环依赖.多例的循环依赖.带AOP的循环依赖.以下是一些结论 ...
- Spring如何处理循环引用
Spring如何处理循环引用 一,划重点 Spring处理循环依赖记录,要是写错了,喷轻一点. 解题核心: 1.AbstractBeanFactory的2个getSingleton方法 2.early ...
- Spring当中循环依赖很少有人讲,今天让我们来看看吧
网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...
- Spring当中循环依赖很少有人讲,今天一起来学习!
网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...
最新文章
- 【风控系统】风控中心—京东基于Spark的风控系统架构实践和技术细节
- JVM内存模型和类加载运行机制
- 语言中的petchar运用_自闭症儿童语言障碍家庭训练,需要融入这些方法
- 1-springboot基础
- Mysql 中的SSL 连接
- Discuz!NT3.0博客扩展
- Atitit 外观ui调整法 表单与表格列表
- SQL Server2005彻底卸载
- excel怎么设置打印区域_淘宝卖家想要打印快递单怎么设置
- MATLAB图像形状识别
- 抖音视频怎么下载MP4格式怎么转换为MP3
- 如何使用python将数据写入txt文件
- 谁是史上最强-用爬虫分析IMDB TOP250电影数据
- Illegal character: U+00A0
- USYD悉尼大学DATA1002 详细作业解析Module5
- Linux配置自动获取ip方式和静态ip方
- 【R语言】ggplot2作图补充(1)
- 阿里云OSS上传下载跨域问题
- ug12在win8计算机名错,Win8安装UG9.0时出错提示“UGII_TMP_DIR 被设为一个有无效(非ASCII)字符的目录”怎么办...
- Ubuntu16.04搜狗拼音输入法候选栏无法显示中文(英文乱码)