1. 前言
Spring在较新版本中已经默认不允许bean之间发生「循环依赖」了,如果检测到循环依赖,容器启动时将会报错,此时可以通过配置来允许循环依赖。
spring.main.allow-circular-references=true
什么是循环依赖?
循环依赖也叫循环引用,简单点说,就是bean之间的依赖关系形成了一个循环,例如beanA依赖beanB,beanB又依赖beanA。
@Component
public class A {
@Autowired
B b;
}
@Component
public class A {
@Autowired
B b;
}
如上代码所示,Spring本身是支持循环依赖的,Spring加载bean时,首先会实例化A,然后对A做初始化,其中就包含属性填充,填充属性时发现A依赖B,于是Spring又会从容器中去加载B,创建B时发现B又依赖了A,循环依赖就此产生,Spring是如何打破这个循环的呢?
2. 依赖注入的方式
Spring完成依赖注入的方式有三种:
- 属性注入
- Setter方法注入
- 构造函数注入
对于构造函数注入的方式,循环依赖是无解的,Spring也无法支持。属性注入和Setter方法注入原理上是一样的,本文通过属性注入的方式来分析Spring是如何支持循环依赖的。
循环依赖仅限于单例bean,对于原型bean,循环依赖也是不允许的。
3. bean的多级缓存
Spring支持循环依赖的核心是bean的三级缓存。
Spring会在启动的时候,加载容器内所有非lazy的单例bean,方法是DefaultListableBeanFactory#preInstantiateSingletons()
,该方法会把容器内所有非lazy的单例beanName拿出来,然后依次调用getBean()
方法获取bean。
getBean()
时,首先会调用getSingleton()
方法优先从缓存中获取bean,只要缓存中存在bean,就可以直接获取了,避免二次创建。
核心就在getSingleton()
方法中,Spring会在各级缓存中查找bean。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 检查一级缓存,是否存在bean
Object singletonObject = this.singletonObjects.get(beanName);
// bean是否创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
/**
* 二级缓存查找
* 二级缓存意义不大?没有二级缓存也能解决循环依赖,作用是啥?
* 三级缓存的ObjectFactory#getObject()方法是有代价的,会触发后置处理器获取早期引用
* @see SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference
* 频繁触发势必带来性能问题,引入二级缓存,一旦getObject()触发就移入二级缓存
*/
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;
}
该方法做了几件事:
- 一级缓存中是否存在bean?存在直接返回bean。
- 一级缓存没有,且bean正在创建中,则去二级缓存查找。
- 二级缓存没有,且允许循环依赖,则去三级缓存查找。
- 三级缓存并没有直接存储bean,而是存储bean的工厂方法对象,通过调用工厂方法来获得bean,一旦通过三级缓存得到了bean,则将其从三级缓存移除并加入到二级缓存中。
所谓的多级缓存,其实就是三个Map容器。
/**
* 一级缓存,完整的bean容器
* 经历了完整生命周期的bean
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/**
* 二级缓存,提前暴露的bean,只实例化了,还未初始化
*/
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/**
* 三级缓存,不直接缓存bean
* 而是提前暴露的bean的工厂方法,调用方法来产生bean
* 对于需要AOP增强的bean,此时就需要生成带来对象了
*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
Spring解决循环依赖的思路是:当beanA还没有完全创建完毕时,就提前暴露这个半成品beanA到三级缓存,接着在创建beanB时,直接引用半成品beanA,由此来打破循环。
Spring创建A和B两个bean时,流程是这样的:
- getA时,一级缓存没有,且不在创建中,于是去createA。
- createA之前,先把A加入到singletonsCurrentlyInCreation,表示创建中。
- A实例化完毕,还未初始化前,先放入三级缓存singletonFactories。
- A填充属性时,发现需要B,去getB。
- getB时,一级缓存没有,且不在创建中,于是去createB。
- B填充属性时,发现需要A,去getA。
- getA发现三级缓存有,将其移入二级缓存,返回A。
- B创建完毕,A创建完毕。
4. 二级缓存的意义
仔细看看getSingleton()
方法,会发现二级缓存好像很多余,没有二级缓存丝毫不影响循环依赖。二级缓存的作用好像就是承接了bean从三级缓存往二级缓存里迁移而已。真的是这样吗?
二级缓存还是有存在的意义的,因为只要Spring允许循环依赖,就会在创建bean的时候提前暴露到三级缓存,但是循环依赖并不一定会产生,也就是说,暴露到三级缓存里的bean其实未必会被提前引用。只要循环依赖没有产生,bean没有被提前引用,就不用触发Spring的扩展点去生成提前暴露的bean对象了。因为获取提前引用的bean对象是有代价的,bean需要被Spring的扩展点处理,如果bean需要被AOP增强,此时会提前创建代理对象。有了二级缓存,就可以保证只有在真的发生了循环依赖的情况下,才去真正的提前暴露bean对象。
Spring会在bean还没有完全创建完毕时,提前将半成品bean暴露到三级缓存中,代码在AbstractAutowireCapableBeanFactory#doCreateBean()
。
当Spring调用ObjectFactory#getObject()
获取这个提前暴露的bean,其实会触发AbstractAutowireCapableBeanFactory#getEarlyBeanReference()
。
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;
}
获取提前暴露的bean引用,这个方法的调用是有代价的,它会触发容器内所有的SmartInstantiationAwareBeanPostProcessor扩展点,调用getEarlyBeanReference()
方法来获取bean。如果这个bean需要被AOP增强,此时就会基于bean创建代理对象了。
也就是说,三级缓存和二级缓存其实是互斥的,只要调用了三级缓存里的ObjectFactory#getObject()
方法拿到了bean,三级缓存就没用了,可以直接往二级缓存里放了。