Spring循环依赖解决姿势
前提
-
写过代码
-
了解Spring
哈哈,只要符合上面2个,那么我们就边走边看,源码解读带你了解Spring对循环依赖的姿势
概念说明
- 什么是循环依赖,同一个项目有如下结构的类
铺垫
首先我们来对Spring的主线过下,spring容器启动其实就主要2点实例化和初始化
实例化
- 对bean进行创建,但是还未对成员变量赋值
初始化
- 对创建的bean里的成员变量赋值,和相关的初始化方法执行
进入Spring循环依赖的姿势
前提概要
认识循环依赖的姿势 :三级缓存
DefaultSingletonBeanRegistry.java
一级缓存 存放实例化和初始化完成的成品对象
/** Cache of singleton objects: bean name to bean instance.*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
三级缓存 key,value; value就是ObjectFactory 一个函数式,循环依赖中主要是存放lambda表达式
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
二级缓存 循环依赖中存放半成品对象
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
工程搭建
工具:idea
Spring版本:5.1.6.RELEASE
- 打开idea工程,准备以下简单Spring工程,只需要依赖spring-context
- A类
package com.spring.cycle;
public class A {
private B b;
public B getB() {
return b;
}
public void setB(B b) {
this.b = b;
}
}
- B类
package com.spring.cycle;
public class B {
private String name;
private A a;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public A getA() {
return a;
}
public void setA(A a) {
this.a = a;
}
}
- 配置文件cycle.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="a" class="com.spring.cycle.A">
<property name="b" ref="b"></property>
</bean>
<bean name="b" class="com.spring.cycle.B">
<!-- 通过属性的set方法给对象赋值 -->
<property name="a" ref="a"></property>
<property name="name" value="王老五"></property>
</bean>
</beans>
- 测试类
package com.spring.cycle;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestCycle {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("cycle.xml");
A a = (A) applicationContext.getBean("a");
B b = (B) applicationContext.getBean("b");
System.out.println(a);
System.out.println(a.getB());
System.out.println("--------------------");
System.out.println(b);
System.out.println(b.getA());
System.out.println(b.getName());
}
}
撸源码
核心几个方法
getBean->doGetBean->createBean->doCreateBean->createBeanInstance->populateBean
Spring核心主流程可以看下前面的文章
Spring启动主线流程源码分析
Spring容器启动中XML解析
- 针对上面简单循环依赖debug走起
1 首先创建A对象
2 先从缓存获取,第一次创建A肯定不存在,返回空
3 开始创建A对象
4 进入getSingleton方法
5 执行getSingleton方法中的singletonFactory.getObject(),就走到函数式接口,开始执行createBean方法
6 开始创建和初始化成员变量
7 createBeanInstance创建对象
populateBean给A对象初始化成员变量
8 找出A对象的成员变量列表,发现存在一个成员变量b
9 遍历A对象的成员变量列表,一个一个设置成员变量
10 给A对象设置成员变量b时,又从bean工厂获取b对象
11 此时给A对象的b成员变量赋值,走到创建B对象流程
12 创建B对象还是先从缓存获取对象
13 创建B对象又走到getSingleton方法,先设置B对象创建标识,其实就是Set数据结构设置一个bean的名字。this.singletonsCurrentlyInCreation.add(beanName)
14 创建B对象开始createBeanInstance,创建后可以看到B对象里面的成员变量name和a都是空的
15 创建B对象刚刚完成后,先放入三级缓存
16 同时此时可以看到三级缓存已经存在a和b的创建对象后的一个lambda表达式
同时把对应的二级缓存清理该bean的信息
17 B对象创建完成且放入到三级缓存后,然后开始给B对象的name和a成员变量赋值
18 先获取到B对象的成员变量列表(a和name)
先设置成员变量a
因为成员变量是RuntimeBeanReference一个引用,所以又从bean工厂获取a对象
还是先从缓存获取a对象
此时三级缓存中存在a对象应用,所以执行a对象引用的lambda表达式获取a的一个半成品对象(a对象的成员变量b还是空),放入二级缓存,清理在三级缓存的a对象信息
A对象获取到后赋值给B对象的成员变量a
此时是给B对象赋值成员变量name,因为是string类型,直接设置即可
19 此时完成B对象创建和B对象的成员变量的赋值
后面的initializeBean方法功能,主要判断是否实现了某些接口,执行相应的方法。实现了InitializingBean接口的类,就在这个函数里执行afterPropertiesSet方法的,所以这个时候类已经实例化完成,但是成员变量不一定赋值,也就是可能存在一个半成品对象的情况
20 B对象已经创建和实例化完成,清理类创建标识this.singletonsCurrentlyInCreation.remove(beanName),然后加入一级缓存,清理对应的二三级缓存信息
21 回到A对象赋值成员变量的现场,刚刚饶了一大圈才创建的B对象,目的是为了给A对象赋值成员变量b
此时看到的A对象已经是一个成品对象,里面的b也是一个成品对象
接下来也是执行A对象的initializeBean方法,完事后就执行创建对象后的收尾工作
此时的A对象才刚刚结束,继续下一个bean的创建
至此A,B类的实例化和初始化已经都完成了。循环依赖过程都通过断点形式走了一遍。
总结
整个按理Spring创建bean和初始化过程如下
问题:如果只有一级缓存能不能解决循环依赖?
不行
从上面调试过程中,发现一个对象存在成品和半成品的状态,如果都放入一个缓存里面,那么其他对象获取到一个半成品对象使用,就会报错,因此需要区分出成品对象和半成品对象的存放地,那么就至少需要二级缓存。一级缓存存放成品对象,二级缓存存放半成品对象。
问题:循环依赖是否只要二级缓存就可以解决?
如果整个项目中没有AOP功能,二级缓存可以解决循环依赖。
修改代码验证. 只从1,2级缓存获取
实例化后直接加入二级缓存
1 修改上面二处代码,我们直接运行我们的测试类,发现完全可以支撑循环依赖
问题项目中存在AOP功能,只有二级缓存会存在什么问题
我们代码加入AOP功能,执行运行原始代码
然后我们修改回原始代码,对于AOP完全可以
问题:为什么构造函数不支持循环依赖
因为构造函数在对象实例化过程中发生的,不会把实例化的信息放入缓存,所以会导致死循环。而通过setter方式解决循环依赖的核心是因为把对象分成实例化和初始化2步,并且实例化完成后会把实例化(未初始化)的对象放入3级缓存中,后面注入时就可以从缓存获取,从而解决循环依赖
关于AOP的详细介绍,我们在后面再次详解