1,什么是循环依赖:
在spring中,对象的创建是交给Spring容器去执行的,Spring创建的Bean默认是单例的,也就是说,在整个Spring容器中,每一个对象都是有且只有一个。
那么这个时候就可能存在一种情况:
比如说,有一个A对象,它有一个b属性,还有一个B对象,它有一个a属性。当在对这两个对象进行属性赋值的时候,就会产生循环依赖问题。
假设先创建A对象,首先对A对象进行实例化,对A对象实例化完成之后,紧接着要对A对象进行初始化步骤中的属性赋值操作,那么在对b属性赋值的时候,此时是没有B对象的,
所以接着要开始创建B对象了,当创建了B对象,并对B对象进行实例化完成之后,接着要对B对象进行初始化步骤里边的属性赋值操作——对a属性进行赋值了,而这个时候A对象也还没有创建完成。
所以这个时候,就形成了一个闭环,形成了一个循环依赖,也就是出现了循环引用的问题,这个时候程序就会报错,提示程序无法继续执行下去了。
这是循环依赖最直接、最简单的一个例子,不管循环依赖是两个对象之间,还是三个对象、四个对象、五个对象等等,最后产生了循环依赖的原因都是如此。
当前这种情况形成了一个闭环,即产生了循环依赖问题。
循环依赖其实就是循环引用,就是两个或者两个以上的bean互相依赖对方,最终形成闭环,然后程序就会报错,提示程序无法继续执行下去了。
2,Spring中循环依赖分两类:
- 构造函数引起的循环依赖
- field属性引起的循环依赖
其中,构造器的循环依赖问题是无法解决的,只能拋出异常,
在解决属性的循环依赖时,spring采用的是 提前暴露对象的方法。
在Spring中,单例对象的初始化主要分为三步:
第一步(createBeanInstance):实例化,其实也就是调用对象的构造方法实例化对象;
第二步(populateBean):初始化--填充属性,这一步主要是对bean对象的属性进行填充,即注入属性、满足依赖;
第三步(initializeBean):初始化--增强处理,这一步之后该Bean就可以使用了。
从上面单例bean的初始化可以知道:循环依赖主要发生在第二步--填充属性,所以我们要解决循环引用也应该从这个过程着手。
对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,
Spring为了解决单例的循环依赖问题,使用了三级缓存。
spring解决循环问题的关键点在于两个方面
1.基于Java的引用传递:即,可以先实例化对象,实例化对象之后,在内存中就有了该对象的内存地址了,我们就可以先从内存中获取到该对象了。
2.单例模式中,同名对象有且仅有一个
在对象的创建过程中,我们可以将对象分为两种状态:
- 一种是完成实例化以及初始化的对象,这种我们将其叫做“成品对象”;
- 另外一种是完成了实例化、但是还没有完成初始化的对象,这一种对象我们将其叫做“半成品对象”。
现在存在A,B两个对象,A中需要注入B,B也需要注入A,现实例化出一个A实例,接着注入B但此时容器中没有B实例,此时不在初始化A,转而去实例化出一个B实例,往B中注入A,此时A已完成实例化,虽然A没有进行初始化是个半成品,但因为再容器中有且仅有一个A,只要A完成初始化,B中引用的A就会是一个完成品
3,三级缓存
这三级缓存分别指:
(三级)singletonFactories : 单例对象工厂的cache,存放 bean 工厂对象;
(二级)earlySingletonObjects :提前曝光的单例对象的Cache ,用于存放已经实例化、但是还没有初始化填充属性的 bean 对象,(尚未填充属性);
(一级)singletonObjects:单例对象的cache,用于存放已经完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用。
PS:什么是提前曝光的对象:
所谓提前曝光的对象就是说,这是一个不完整的对象,它的属性还没有赋值,对象也没有被初始化-增强处理,它仅仅是被创建出来了(在内存中开辟出内存空间了、有内存地址了)。
这就是提前曝光的对象。
三级缓存的大致工作流程:
1.在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存指的是一级缓存singletonObjects。
2.如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。
3.如果还是获取不到,就从三级缓存中获取--singletonFactory.getObject()方法;
4.如果 在三级缓存中 获取到这个单例Bean了,则:从singletonFactories中移除该Bean,并放入earlySingletonObjects中,其实也就是从三级缓存提升到了二级缓存中,目的是将该Bean提前曝光。
.从上面三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个第三级缓存。这个cache的类型是ObjectFactory。这里就是解决循环依赖的关键,
在 createBeanInstance实例化Bean 之后,也就是说单例对象此时已经被实例化出来了。
这个对象已经被生产出来了,虽然还不完美(还没有属性赋值和增强实现,还是一个半成品对象),但是已经能被引用了(因为可以根据引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来,让其他的对象使用。
举例:
假如说,“A的某个属性依赖了B的实例对象,同时B的某个field属性依赖了A的实例对象”这种循环依赖的情况。
A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories三级缓存中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去获取B对象,
结果发现B还没有被创建,所以走创建B对象的流程,B对象在初始化属性赋值的时候发现自己依赖了A对象,于是尝试获取A对象,尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过三级缓存中的ObjectFactory.getObject()方法拿到A对象(虽然A此时还是一个半成品对象),并将其放入到二级缓存中、从三级缓存中移除。
B拿到A对象后,顺利完成了初始化阶段,之后B对象将自己放入到一级缓存singletonObjects中。
此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段,最终A也完成了初始化,进入了一级缓存singletonObjects中,
所以最后,A和B对象都完成了初始化。
关于三级缓存的一些问题
1,三个缓存对象,在获取数据的时候,是按照什么顺序来获取的?
答:先从一级缓存中获取对象,如果没有再从二级缓存中获取,二级缓存中没有再从三级缓存中获取。
所以当前面级别的缓存中存在了对象,那么后面级别的缓存就需要把该对象给移除。
2,如果只有一级缓存,能解决循环依赖问题吗?
不能,如果只有一级缓存,那么成品对象和半成品对象会放到一起,这个是没办法区分的,所以需要两个缓存来分别存放不同状态的对象,一级缓存-存放成品,二级缓存-存放半成品。
3,如果只有一级和二级缓存,能否解决循环依赖问题?
在刚刚的整个流程中,三级缓存一共出现了几次? getsingleton , doCreateBean。
如果对象的创建过程中不包含 aop ,那么一级二级缓存就可以解决循环依赖问题,
但是如果包含 aop 的操作,那么没有三级缓存的话,循环依赖问题是解决不了的。
4,为什么添加了AOP的操作之后,就必须需要三级缓存来解决循环依赖问题?
在创建代理对象的时候,是否需要生成原始对象?答案肯定是需要。
当创建完成原始对象之后,后续有需要创建代理对象,那么此时就是一个 beanName 对应有两个对象,(原始对象和代理对象),那么在通过beanName引用的时候应该使用哪一个对象?
在整个容器中,有且仅能有一个同名的对象,当需要生成代理对象的时候,就要把代理对象覆盖原始对象。
程序是怎么知道在什么时候要进行代理对象的创建的呢?
那么就需要一个类似于回调的接口判断,当第一次对外暴露使用的时候,来判断当前对象是否需要去创建代理对象;
而三级缓存加了什么操作?添加了一个 getEarlyBeanReference 的方法。
getEarlyBeanRefenerce 方法里边的 if 判断:
如果需要代理,就返回代理对象,
如果没有代理,就返回原始对象。