spring中的循环依赖
及三层map
解决方案,八股文中重灾区,强如小伙伴们或许一字一句倒背如流了,当年的我也是如此,然而现实狠狠给了我一巴掌,报错虽迟但到
报错一:
The dependencies of some of the beans in the application context form a cycle
报错二:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘c’: Bean with name ‘c’ has been injected into other beans [d] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.
以上都是循环依赖导致,我纳闷了,不是说spring已经解决的循环依赖问题吗,为什么还会报错,说好的《java面试宝典》《java架构师指南》
诚不欺我呢!
在探索哪些场景会报错之前,让我们先来回顾一下,到底什么是循环依赖
spring 字段注入,相互持有对方引用,造成循环依赖的典型场景
@Service
class A {
@Resource
private B b;
}
@Service
class B {
@Resource
private A a;
}
有的小伙伴们开始想了,这种关系我直接用代码表示出来很简单呀,直接new
出来,然后设置一下值就好了,为什么用spring的方式就出现循环依赖问题了。
java代码解决方案如下
A a = new A();
B b = new B();
//依次注入对方
a.setB(b);
b.setA(a);
对比发现,很容易得出以下结论:spring在ioc进程中,
对象的生成
需要流转复杂的bean生命周期
,这才导致对象互相引用的时候产生了循环依赖
问题。
问题又来了
那什么是bean的生命周期
呢
bean的生命周期,由
主线
和扩展点
构成。
本文章只需了解主线流程即可,其中主线为红色
,扩展点为蓝色
。每个bean对象的生成,都遵循以上过程。
那么,问题显而易见,bean A
创建的时候走到空对象填充值
节点时发现需要填充bean B
,进而去创建bean B
,bean B
创建的时候走到空对象填充值
节点时发现需要填充bean A
,进而去创建bean A
,好家伙,进入死循环了。这就是循坏依赖
。
回过头来想,我们自己写java代码
的时候,很容易模拟并解决这个对象相互引用
的场景呀,为什么spring做不到呢,究其根本,我们在创建对象的时候是可以拿到其他对象的引用
,而spring对于没有创建完成的bean是不会加入到单例池中区的,意味着其他对象的引用
是不可见
的。见节点spring缓存冲准备就绪的bean
节点,最后才会加入到singletonObjects
缓存中。
我们都知道彼此的存在,但我们都是瞎子。
所以,spring提供了三层map的机制,来解决bean中间状态的可见问题,核心逻辑如下。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 第一层缓存 创建好的bean
Object singletonObject = this.singletonObjects.get(beanName);
//isSingletonCurrentlyInCreation 代表获取的类已经开始初始化了,这里说明已经造成循环依赖了。
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//第二级缓存,解决中间状态bean对象的可见性问题。
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
//第三级缓存,解决动态代理的幂等性。
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
按访问顺序map排序
singletonObjects
存的是创建好的beanearlySingletonObjects
半成品bean(实例化的空对象,还未填充值),由singletonFactories
工厂生成。singletonFactories
提前进行aop代理,生成最终的代理类,保证引用不会变化。spring的思路很简单,空对象填充值
节点前,通过把半成品bean
包装成工厂类放入singletonFactories
提前暴露对象引用来保证可见性
。
//如果支持循环依赖的话就会进行提前暴露
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");
}
//提前暴露,保证其他bean的可见性
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// Initialize the bean instance.
Object exposedObject = bean;
try {
//空对象填充值
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
} catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
} else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}
提前暴露的能力靠第二层map就能实现了,为什么还需要搞个工厂来作为第三层map呢?
spring要保证,从map中拿到的引用是最终的引用,即对象的引用不会再改变了。我们想想哪些操作会导致对象引用会改变?大多数情况答案是:spring aop,spring aop的自动化编织,通过扩展点调用BeanPostProcessor的postProcessAfterInitialization()方法
,定义BeanPostProcessor
来实现,这将会导致对象引用的变化,工厂类的作用就是提前来进行aop操作,来保证获取到最终的对象
。
工厂类调用此方法生成第二层map对象
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;
}
AbstractAutoProxyCreator aop自动编织的实现类,对该方法的重写,提前生成最终对象
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
//编织切面
return wrapIfNecessary(bean, beanName, cacheKey);
}
从源码分析,可能存在不止AbstractAutoProxyCreator
一种BeanPostProcessor
会改变代理类的引用,spring aop 改变代理类只是大部分的情况,如果存在其他会改变代理类引用
的BeanPostProcessor
一定要检查是否重写了getEarlyBeanReference
方法,不然出现循环依赖的时候将会给你小惊喜
。
为什么我不叫它三级缓存
呢?
我理解的层级缓存,应该是一级缓存没有,去二级缓存找,二级缓存没有去三级缓存找,类似cpu的L1L2L3缓存
,每层缓存都可能会有同一个数据。而spring更像是三层map
,因为bean对象实际上只会在其中一个map中。
纵观spring循坏依赖解决方案,还是有几个小问题:
报错一
@Service
class A {
private B b;
@Autowired
public A(B b) {
this.b = b;
}
}
@Service
class B {
private A a;
@Autowired
public B(A a) {
this.a = a;
}
}
spring循环依赖解决的就是就是中间状态对象的可见性
问题,earlySingletonObjects
层会缓存中间状态对象引用,如果beanA需要在构造函数结束前去创建beanB,这样无法产生中间状态的beanA,无法满足可见性问题,从而报错一
;
解决方式如下
@Service
class A {
private B b;
@Autowired
public A(@Lazy B b) {
this.b = b;
}
}
通过添加@Lazy
注解解决,具体在创建beanA时候引入beanB,不会真正去创建beanB,而是得到一个由@Lazy
生成的代理类,beanA拿到该代理类创建完毕放入singletonObjects
,beanB在真正使用的时候开始创建,这个时候beanA已经在singletonObjects
中了,自然不会发生循坏依赖问题·。
BeanPostProcessor
改变代理类的引用,没有重写getEarlyBeanReference
方法,对应报错二
@Service
@Async
@EnableAsync
class C {
@Resource
private D d ;
}
@Service
public class D {
@Resource
private C c;
}
@Async
会通过代理,通过BeanPostProcessor
扩展点增强类的功能,提供类方法异步的能力,新生成的代理类会改变原始类对象的引用。我们来看看具体的实现类AsyncAnnotationBeanPostProcessor
。
好家伙,完全没有重写getEarlyBeanReference
,结果报错二
,出处如下。
//earlySingletonReference 循环引用生成的代理类
//exposedObject 本方法生成的代理类
//bean 原始引用
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
//如果走到这里,说明触发了循环依赖
if (earlySingletonReference != null) {
//如果相等,这里是指发生了aop根据cacheKey判定到幂等
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
//走到这里说明有其他钩子(后置处理器)把bean替换了,所以要检查在此之前是否已经发生过该bean的依赖注入,如果发生,就导致一个bean的不同版本被注入,针对这种情况,会抛出异常
} else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
在doCreateBean
核心方法处以前看不懂的代码也可以解释了,代理对象的引用发生变化,导致spring认为有两个不同版本
的bean加入到了容器内,对于这种情况spring也无能为力,只得抛出一个错,告诉程序员,有人在害你。
可见性
。BeanPostProcessor
改变对象引用的情况下,需要查看是否会发生循环引用
,如果是,需要查看是否重写了getEarlyBeanReference
方法,否则会报错。