单例在spring里的获取方式
今天讲一下spring中针对单例bean的循环依赖问题,本着追本溯源的学习理念,我们要先知道单例在spring中怎么管理的。spring获取实例都通过beanFactory的getBean方法获取实例,顺着代码而下,在doGetBean方法(AbstractBeanFactory)中,单例总是通过getSingleton()方法获取实例。
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
......
// Create bean instance.
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
......
}
getSingleton和createBean方法大致流程(单例):
- 先从缓存拿(从第一层到第三层缓存中依次获取)
- 创建对象准备工作(beforeSingletonCreation):把beanName标记为正在创建中(singletonsCurrentlyInCreation.add)
- 对象实例化(createBeanInstance):,通过其定义里的class找到构造器方法反射创建实例
- 提前暴露对象解决循环引用问题(Eagerly cache singletons to be able to resolve circular references):如果此beanName为正在创建中,则把其对象工厂放入第三层缓存(addSingletonFactory)
- 有一个bean拓展点,getEarlyBeanReference 是一个延迟动作,等其它类依赖获取这个对象的时候会触发(SmartInstantiationAwareBeanPostProcessor,本质也是BeanPostProcessor)。
- 对象初始化:
- populateBean 装配对象变量:获取此bean中有@Autowired等注解的成员变量,从所有bean定义中找出此类型的beanName,又通过BeanFactory#getBean方法获取实例,然后反射设值成员变量.
- initializeBean:我们熟知的 applyBeanPostProcessorsBeforeInitialization (BeanPostProcessor) 和 InitializingBean 和 applyBeanPostProcessorsAfterInitialization (BeanPostProcessor)等拓展点.
- 循环依赖检查
- 创建对象结束工作(afterSingletonCreation):移除正在创建中的标记(singletonsCurrentlyInCreation.remove),把实例放入第一层缓存,移除第二、三层中的缓存(addSingleton),最后返回实例
不熟悉源码的同学可能看着云里雾里,其实这里得分成两块来看:标黑的是创建bean的主流程,其他的是发生循环依赖(bean相互引用)时的处理。
三级缓存
Spring给了三个map,也就是三级缓存:
- 一级缓存 : singletonObjects 初始化完后的单例都会存在这个map中,一般ioc容器初始完后,你再用getBean取实例都可以从这里获取到。
- 二级缓存 :earlySingletonObjects 提前曝光的实例,这时候bean还处于创建中(bean的创建分为 实例化 和 初始化),没有完全创建好。
- 三级缓存 :singletonFactories 跟二级缓存要放在一起看,这里存的只是一个工厂类,需要通过getObject获取实例,实例获取到以后会放到二级缓存,只会执行一遍。
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
再来看看Spring对三级缓存的获取逻辑,这段代码的含义就是 先从一级缓存中获取实例,取不到,再去取二级缓存,再没有,那就三级中取。 注意: 当从三级中取到实例时,会删除三级缓存中的值,然后放入二级缓存,二/三级缓存是一体的,三级缓存的工厂类是为了延迟执行(怎么延迟执行后面说),然后二级缓存是为了存放三级缓存getObject的结果,所以每个对象的三级缓存只会执行一遍,所以其实我们就把三级缓存当成两级缓存(一级缓存和二/三级缓存),这样对整体机制的理解有帮助,减少理解的成本
。
protected 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 != NULL_OBJECT ? singletonObject : null);
}
我们还要知道Spring对一个bean的拓展点有哪些,这对我们理解Spring循环依赖处理有帮助。
从上文我们知道一个bean在创建过程中因为拓展点的存在,可能会产生两个对象(狸猫换太子):
- 一个是循环依赖时需要设值依赖的对象(getEarlyBeanReference)
- 一个是初始化后的对象(initializeBean)
如果现在要对bean做增强,比如实现切面,则需要生成代理类,所以spring在上述两个方法中通过BeanPostProcessor类提供了拓展点。
当我们铺垫完上面的基础源码流程,我们开始引出本文的核心:什么是循环依赖和Spring循环依赖解决方案。
循环依赖的问题和Spring的解决方案
上一章我们讲了单例如何从三个缓存中去取,可是为什么要设计这个缓存我们听的云里雾里,这里我们就抛出开发中存在的问题:循环依赖。
我们知道,当A 有一个 属性 B , B 有一个属性A时,就形成了循环依赖,按照单例bean的创建逻辑,A没创建完,就去拿B,B还没创建完,又去拿A,请问A在哪?A还没创建完呢 ! 所以解决问题的关键在哪?spring给设计了一个多级缓存,当A完成实例化(createBeanInstance)时,这个实例已经有了自己的内存地址,只是对象不够完善,spring先把这种对象放进一个缓存。然后装配属性的时候需要B时 ,B开始自己的创建之旅,途中B需要A了,它就可以从缓存中顺利拿到实例A,即使这时候的A并不完善。 然后B把自己创建完成后,又轮到A继续完善自己,直到 A和B 都创建完毕,这样就没有了循环依赖的问题。
下面用图反映spring bean的大致创建流程 与 多级缓存之间的配合:
- 当实例化完一个初始bean时,会先放入三级缓存,记住,这里的A还只是一个 初生的婴儿,没有属性装配,没有经历初始化,是个ObjectFactory。
- 然后经历装配bean属性的过程。期间属性涉及到 循环依赖的时候,就可以通过第三缓存拿到对象。拿到后删除三级缓存,放入二级缓存。
- 实例创建完毕后会删除二,三级缓存,放入一级缓存。一级缓存就是 spring单例对象的完全体,后面程序可以通过beanFactory 随时取用。
在经历拓展点情况下的循环依赖检查
在了解了解决循环依赖的基本原理后,我们要知道这种情况只能解决最理想的循环依赖情况(没有经历拓展点的Bean),但是Spring预留了两处拓展点,拓展点意味着Bean的返回值有可能跟原始Bean不再一样,比如原始Bean是A,那么A跟其它对象的依赖的A必须要内存地址一致才行,这时候看看Spring怎么检查循环依赖的。
我们重点关注一下下面的代码,从doCreateBean方法中看到如下示例代码,这段代码在装配完bean(populateBean)后开始执行,会看到其中调用了getSingleton(beanName, false),这里其实是 这个bean是否存在被其它bean依赖的一次检查
:
这里有三个对象我们需要留意,一个是earlySingletonReference
,一个是exposedObject
,一个是 bean
。
bean是最原始的对象,不会被任何流程修改。
earlySingletonReference,代表这个bean是否存在被其它bean依赖的情况。因为第二个参数为false,看里面的实现可以知道,false的话是不会执行工厂类的方法的,换句话说,一定是哪里依赖了这个对象,才会执行了工厂类的getObject方法,earlySingletonReference的值才不为空。
当发现这个bean是存在被其它bean依赖的情况(earlySingletonReference!=null),Spring就要开始提高警惕了,Spring会再判断exposedObject == bean,exposedObject 是可能被initializeBean这个拓展点修改的,如果相等,那么一切都好说,Spring通过把exposedObject 的引用指向了二级缓存中的对象,从而保证不管是A本身也好,还是其它对象依赖的A也好,都能保证一致。
如果不相等,那就麻烦了!这代表着其它对象依赖的A与exposedObject 是不一样的对象, 代码会开始找哪些对象会依赖B(根据beanName查找),最终发现还真不少(actualDependentBeans不为空),然后就会开始报错,项目启动失败。
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<String>(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 " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
所以,为什么是三层缓存
在前面说了那么多的情况下,可能大家还是没理解为什么要用三层缓存,甚至觉得Spring是过度设计。
不知道大家有没有理解getEarlyBeanReference,按照注释,它是为了解决循环依赖的,那么对于Spring IOC的主体流程中,getEarlyBeanReference被设计成需要的时候才执行,什么时候是需要的?那当然是存在相互依赖的时候!所以Spring设计了一个可以延迟执行的ObjectFactory,所以Spring的三层缓存才被设计出来
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
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");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
最后谈谈AOP跟拓展点
aop具体的原理可以看我aop的文章 spring之我见 - Spring AOP实现原理(上), spring之我见 - Spring AOP实现原理(下)
. 我这里直接说一下结论,有两个地方: 一个是BeanPostProcessors 的 postProcessAfterInitialization逻辑里,还有一个就在 getEarlyBeanReference 方法中, 这两个方法同属于 AbstractAutoProxyCreator 抽象类(继承BeanPostProcessor),在创建bean的过程中会先执行 getEarlyBeanReference ,再执行postProcessAfterInitialization,两个方法都是创建代理对象的方法,但是会通过一个cache map避免重复执行,具体执行哪一个要视具体情况来定。
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyBeanReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
.......
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyBeanReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
总结
总结三层缓存的话
第一层缓存:存放的是成品bean,项目启动完成后获取bean实例时,就会从这里取。
第二级缓存:创建bean过程中用于处理循环依赖的临时缓存,搭配第三层缓存,保证第三层缓存的ObjectFactory只执行一次
第三层缓存:创建bean过程中用于处理循环依赖的临时缓存,只有在存在循环依赖的情况下才会触发ObjectFactory的getObject方法(延迟触发),且获取对象会作为当前bean的最终对象
修订
2024.06.12 依据 【超级干货】为什么spring一定要弄个三级缓存?重新修订了本文章,并引用重新组织了部分内容,感谢原作者!!