要讲清楚三级缓存解决set循环依赖问题先要讲清除两个问题,一是spring-ioc
会存在哪几种循环依赖的情况以及哪种循环依赖是无解的,哪种是框架会帮我们处理的
,二是getBean()这个方法干了什么,以及里面用到的三级缓存是怎么优雅的解决循环依赖的
Spring三种循环依赖
引言:循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错。下面说一下Spring是如果解决循环依赖的。
构造器参数循环依赖
表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyIn CreationException异常表示循环依赖。
ServiceA依赖了ServiceB,同时ServiceB依赖了ServiceA,并且都是构造器注入的方式(需要两者都是相互构造器注入)
Demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class ServiceA {
private ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
public class ServiceB {
private ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
走一下:1
2
3
4
5
6
7
8
9
10
11
12 ({ServiceA.class, ServiceB.class})
public class MainConfig {
}
public class DemoApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext annotation =
new AnnotationConfigApplicationContext(MainConfig.class);
}
}
果不其然:1
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'com.springboot.demo.scan.component.ServiceA': Requested bean is currently in creation: Is there an unresolvable circular reference?
why?:
稍后再说吧。。先把另外两种情况再讲下
set单例循环注入
众所周知,如果两个相互依赖的bean有一方是set注入对方的,那么就会成功注入了
Demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class ServiceA {
private ServiceB serviceB;
public ServiceB getServiceB() {
return serviceB;
}
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
public class ServiceB {
private ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
启动和配置bean的代码就不贴了,同上
走一把,你会发现程序正常运行,完全OJBK
set原型方式注入
原型注入的话,循环依赖是一定会报错的
Demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23"prototype") (
public class ServiceA {
private ServiceB serviceB;
public ServiceB getServiceB() {
return serviceB;
}
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
public class ServiceB {
private ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
run一下:1
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'com.springboot.demo.scan.component.ServiceB': Requested bean is currently in creation: Is there an unresolvable circular reference?
总结一下
1.相互都是构造器注入,Spring解决不了
2.set单例注入,Spring可以解决
3.set原型注入,Spring解决不了
Spring循环依赖的理论依据
Spring循环依赖的理论依据其实是Java基于引用传递,当我们获取到对象的引用时,对象的field或者或属性是可以延后设置的。
Spring单例对象的初始化其实可以分为三步:(实例化、填充属性、初始化)
- createBeanInstance, 实例化,实际上就是调用对应的构造方法构造对象,此时只是调用了构造方法。
- populate:populateBean,填充属性,这步对spring xml中指定的property进行populate
- initializeBean,调用spring xml中指定的init方法,或者AfterPropertiesSet方法
会发生循环依赖的步骤集中在第一步和第二步。
大致调用栈绘图如下:
Spring容器的’三级缓存’
在Spring容器的整个声明周期中,单例Bean有且仅有一个对象。这很容易让人想到可以用缓存来加速访问。
从源码中也可以看出Spring大量运用了Cache的手段,在循环依赖问题的解决过程中甚至不惜使用了“三级缓存”,这也便是它设计的精妙之处~
介绍一下三级缓存:
- singletonObjects:用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用
- earlySingletonObjects:提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖
- singletonFactories:单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖
获取单例Bean的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
简单解析:
- 从一级缓存singletonObjects中去获取。(如果获取到就直接return)
- 如果获取不到或者对象正在创建中(isSingletonCurrentlyInCreation()),那就再从二级缓存earlySingletonObjects中获取。(如果获取到就直接return)
- 如果还是获取不到,且允许singletonFactories(allowEarlyReference=true)通过getObject()获取。就从三级缓存singletonFactory.getObject()获取。(如果获取到了就从singletonFactories中移除,并且放进earlySingletonObjects。其实也就是从三级缓存移动(是剪切、不是复制哦~)到了二级缓存)
加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决为什么相互都是构造器注入方式解决不了
大家从getBean()这个方法debug进去看,总的流程可以整理为下图(我抄过来的)
依旧以上面A、B类使用属性field注入循环依赖的例子为例,对整个流程做文字步骤总结如下: - 使用context.getBean(A.class),旨在获取容器内的单例A(若A不存在,就会走A这个Bean的创建流程),显然初次获取A是不存在的,因此走A的创建之路~
- 实例化A(注意此处仅仅是实例化),并将它放进缓存(此时A已经实例化完成,已经可以被引用了)
- 初始化A:@Autowired依赖注入B(此时需要去容器内获取B)
- 为了完成依赖注入B,会通过getBean(B)去容器内找B。但此时B在容器内不存在,就走向B的创建之路~
- 实例化B,并将其放入缓存。(此时B也能够被引用了)
- 初始化B,@Autowired依赖注入A(此时需要去容器内获取A)
- 此处重要:初始化B时会调用getBean(A)去容器内找到A,上面我们已经说过了此时候因为A已经实例化完成了并且放进了缓存里,所以这个时候去看缓存里是已经存在A的引用了的,所以getBean(A)能够正常返回
- B初始化成功(此时已经注入A成功了,已成功持有A的引用了),return(注意此处return相当于是返回最上面的getBean(B)这句代码,回到了初始化A的流程中~)。
因为B实例已经成功返回了,因此最终A也初始化成功 - 到此,B持有的已经是初始化完成的A,A持有的也是初始化完成的B,完美~
- 站的角度高一点,宏观上看Spring处理循环依赖的整个流程就是如此。希望这个宏观层面的总结能更加有助于小伙伴们对Spring解决循环依赖的原理的了解,同时也顺便能解释为何构造器循环依赖就不好使的原因。
如果两个循环依赖的对象都是相互构造器注入,可想而知,在实例化A的时候,此时就需要入参为B的实例化对象的引用,但是此时B的实例化又依赖A的实例化对象引用,这样就GG了
为什么set单例的注入方式可以解决
请参照上图以及上面10个步骤,大家就明白了。
为什么原型的注入方式解决不了
请看源代码,从getBean()方法追进去,会发现一层这样的判断,如果是单例的才执行一系列操作,否则GG。直接第一步createBeanInstance()都不会进去,因为原型bean都是在需要使用的时候再创建一个出来
1 | // Create bean instance. |
思考
但是其实这还存在一层思考,为什么要用三级缓存,按照这个思想,实际两级缓存就可以了吧。
这也是Spring设计的巧妙之处,和AOP相关,我们下次再讨论。