Spring 如何处理循环依赖?

Spring 如何处理循环依赖?

项目环境

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核心编程思想》
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值