Spring 如何处理循环依赖?
文章目录
项目环境
- 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;
@Autowired
private ClassRoom classRoom;
...
ClassRoom 教室类,同样教室里面又包含学生的信息,这里我们用集合表示
public class ClassRoom {
private Long id;
private String name;
@Autowired
private 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();
}
@Bean
public Student student() {
Student student = new Student();
student.setId(1L);
student.setName("小仙");
return student;
}
@Bean
public 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核心编程思想》