spring|spring如何解决循环依赖问题
一、循环依赖 一般来讲,我们说的循环依赖,指得是2个或两个以上的bean 互相持有对方,最终形成闭环的情况。
比如A依赖B,B依赖C,C又依赖A。
文章图片
自已依赖自己,也会发生循环依赖的问题。
文章图片
二、Spring中什么时候会发生循环依赖 我们首先要知道,Spring的单例对象的初始化主要分为三步:
文章图片
- createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象
- populateBean:填充属性,这一步主要是多bean的依赖属性进行填充
- initializeBean:调用spring xml中的init 方法。
注: 为什么说单例对象的初始化。这是因为原型(Prototype)的场景是不支持循环依赖的,通常会走到AbstractBeanFactory类中下面的判断,抛出异常。
if (isPrototypeCurrentlyInCreation(beanName)) {throw new BeanCurrentlyInCreationException(beanName);
}
所以默认的是单例的属性注入。
(1)使用构造器进行依赖注入的时候 (以两个bean为例)
public class StudentA {private StudentB studentB ;
public void setStudentB(StudentB studentB) {this.studentB = studentB;
}public StudentA() {}public StudentA(StudentB studentB) {this.studentB = studentB;
}
}
public class StudentB {private StudentA studentA ;
public void setStudentC(StudentA studentA) {this.studentA = studentA;
}public StudentB() {}public StudentB(StudentA studentA) {this.studentA = studentA;
}
}
执行结果:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
这种循环依赖spring没有办法解决,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。
源码:
文章图片
源码分析:
Spring容器会将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,因此如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常来表示循环依赖;而对于创建完毕的Bean将从“当前创建Bean池”中清除掉。
以上面的程序为例,Spring容器先创建单例A,但是A依赖B,所以将A放在
“当前创建Bean池”中,此时创建B,发现B依赖A, 但是,此时A已经在池中,所以会报错。因为在池中的Bean都是未初始化完的(正在创建的),所以会产生依赖错误 ,(初始化完的Bean会从池中移除)。
(2)使用setter方法进行依赖注入的时候
文章图片
采用三级缓存解决。
结合上图来看,Spring先是构造实例化Bean对象 ,创建成功后,Spring会通过代码提前将对象暴露出来,此时的对象A还没有完成属性注入,属于早期对象,此时Spring会将这个实例化结束的对象放到一个Map中,并且Spring提供了获取这个未设置属性的实例化对象引用的方法。 结合我们的实例来看,当Spring实例化了A、B后,紧接着会去设置对象的属性,此时A依赖B,就会去Map中取出存在里面的单例B对象,以此类推,不会出来循环的问题。
换句话说,对于setter注入造成的依赖是通过Spring容器提前暴露刚完成实例化但未完成初始化的bean来解决的,而且只能解决单例作用域的bean循环依赖。通过提前暴露一个单例工厂方法,从而使其他bean能引用到该bean。
三、解决循环依赖
文章图片
对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。
我们首先看源码,三级缓存主要指:
文章图片
这三级缓存分别指:
- singletonObjects:单例对象的cache,存放可用的成品Bean。(一级缓存)
- earlySingletonObjects:提前暴光的单例对象的cache,存放半成品的Bean,半成品的Bean是已创建对象,但是未注入属性和初始化,用以解决循环依赖。(二级缓存)
- singletonFactories : 单例对象工厂的cache,存的是Bean工厂对象,用来生成半成品的Bean并放入到二级缓存中,用以解决循环依赖。(三级缓存)
获取bean:
文章图片
主要调用**getSingleton()**方法来获取bean;
文章图片
上面的代码需要解释两个参数:
- isSingletonCurrentlyInCreation()判断当前单例bean是否正在创建中,也就是没有初始化完成(比如A的构造器依赖了B对象所以得先去创建B对象,或则在A的populateBean过程中依赖了B对象,得先去创建B对象,这时的A就是处于创建中的状态。)
- allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象。
如果没有获取到,也就是缓存中没有这个bean对象,那就先实例化A,然后再放入三级缓存中。
【spring|spring如何解决循环依赖问题】调用AbstractAutowireCapableBeanFactory类中的doCreateBean方法,实例化A。
文章图片
实例化完成后,调用DefaultSingletonBeanRegistry类中的addSingletonFactory方法,将bean 放入到三级缓存中。
文章图片
这里就是解决循环依赖的关键,这段代码发生在doCreateBean之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用(Spring解决循环依赖的核心思想就是提前曝光)。
这样做有什么好处呢?让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。
知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。
而如果在三级缓存中获取到了bean对象, 则将bean对象从三级缓存singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。
文章图片
四、为什么不用二级缓存? 理论上二级缓存时可行的,只需要将三级缓存中BeanFactory创建的对象提前放入二级缓存中,这样三级缓存就可以移除了。但是,
如果要使用二级缓存解决循环依赖,意味着Bean在构造完后就创建代理对象,这样违背了Spring设计原则。Spring结合AOP跟Bean的生命周期,是在Bean创建完全之后通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。
另一种说法是:初始spring是没有解决循环引用问题的,设计原则是 bean 实例化、属性设置、初始化之后 再 生成aop对象,但是为了解决循环依赖但又尽量不打破这个设计原则的情况下,使用了存储了函数式接口的第三级缓存; 如果使用二级缓存的话,可以将aop的代理工作提前到 提前暴露实例的阶段执行; 也就是说所有的bean在创建过程中就先生成代理对象再初始化和其他工作; 但是这样的话,就和spring的aop的设计原则相驳,aop的实现需要与bean的正常生命周期的创建分离; 这样只有使用第三级缓存封装一个函数式接口对象到缓存中, 发生循环依赖时,触发代理类的生成;
文章图片
五、图解三级缓存流程
文章图片
文章图片
文章图片
文章图片
推荐阅读
- parallels|parallels desktop 解决网络初始化失败问题
- 考研英语阅读终极解决方案——阅读理解如何巧拿高分
- 如何寻找情感问答App的分析切入点
- Activiti(一)SpringBoot2集成Activiti6
- mybatisplus如何在xml的连表查询中使用queryWrapper
- MybatisPlus|MybatisPlus LambdaQueryWrapper使用int默认值的坑及解决
- MybatisPlus使用queryWrapper如何实现复杂查询
- SpringBoot调用公共模块的自定义注解失效的解决
- 解决SpringBoot引用别的模块无法注入的问题
- 如何在Mac中的文件选择框中打开系统隐藏文件夹