所有分类
  • 所有分类
  • 未分类

Spring循环依赖的原理(四)-为什么用三级缓存,而不是二级

简介

说明

本文介绍Spring(SpringBoot)为什么使用三级缓存来解决循环依赖(为什么不使用二级)。

问题引出

在上边的分析中我们可以提出两个问题:

  1. 二级缓存好像没有用到?那么它什么时候会用到?
  2. 为什么第三级缓存要用一个工厂,删除第三级缓存,只用第一二级不可以吗?

系列文章

  1. Spring循环依赖的原理系列(一)–什么是循环依赖 – 自学精灵
  2. Spring循环依赖的原理系列(二)–打断点分析 – 自学精灵
  3. Spring循环依赖的原理系列(三)–原理概述 – 自学精灵
  4. Spring循环依赖的原理系列(四)–为什么用三级缓存,而不是二级 – 自学精灵

可以去掉第三级缓存吗?

不可以去掉第三级缓存。

浅层原因

Spring 的设计原则是在 IOC 结束之后再AOP( bean 实例化、属性设置、初始化之后再通过进行AOP(生成代理对象))。即:AOP的实现需要与bean的生命周期的创建分离。

为了解决循环依赖但又尽量不打破这个设计原则的情况下,使用了第三级缓存(key:bean名字,value:ObjectFactory)。它将一个函数式接口作为ObjectFactory,相当于延迟初始化。在AOP或者解决循环依赖时,通过调用Object的getObject()方法获取到第三级缓存中的对象。

如果去掉第三级缓存,将AOP的代理工作放到第二级缓存,这样的话,bean在创建过程中就先生成代理对象再初始化和其他工作,与Spring的AOP的设计原则相违背。

深层原因

如果创建的Bean有对应的代理,那其他对象注入时,注入的应该是对应的代理对象;但是Spring无法提前知道这个对象是否有循环依赖。正常情况下没有循环依赖,Spring都是在创建好完成品Bean之后才创建对应的代理。这时候Spring有两个选择:

  1. 不管有没有循环依赖,都提前创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入。
  2. 不提前创建好代理对象,在出现循环依赖被其他对象注入时,才实时生成代理对象。这样在没有循环依赖的情况下,Bean就可以按着Spring设计原则的步骤来创建。

Spring选择了第二种方式,那怎么做到提前曝光对象而又不生成代理呢?

Spring就是在对象外面包一层ObjectFactory,提前曝光的是ObjectFactory对象,在被注入时才在ObjectFactory.getObject方式内实时生成代理对象,并将生成好的代理对象放入到第二级缓存:Map<String, Object> earlySingletonObjects。

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

详解

上边:“bean何时被加入第3级缓存?”  我们可以得到,第三级缓存里存放的value是:AbstractAutowireCapableBeanFactory#getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean)。

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;
}

可以看到实际调用SmartInstantiationAwareBeanPostProcessor接口的getEarlyBeanReference(Object bean, String beanName)方法,此接口有以下实现类:

InstantiationAwareBeanPostProcessorAdapter

@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
	return bean;
}

直接返回bean,这里看起来第三级缓存没必要存在。但是,看另一个实现类:

AbstractAutoProxyCreator

@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
	Object cacheKey = getCacheKey(bean.getClass(), beanName);
	this.earlyProxyReferences.put(cacheKey, bean);
	return wrapIfNecessary(bean, beanName, cacheKey);
}

这里把对象放入第二级缓存。

Spring之AOP的原理(一)–概述 – 自学精灵 曾分析过AOP:将切面逻辑织入目标Bean:AbstractAutoProxyCreator#postProcessAfterInitialization方法,在Bean实例化之后被调用。

来看postProcessAfterInitialization

@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
	if (bean != null) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
			return wrapIfNecessary(bean, beanName, cacheKey);
		}
	}
	return bean;
}

把key对应的项从第二级缓存中移除。如果原来第二级缓存中就是这个bean,则返回此bean;否则,返回代理类。

可以去掉第二级缓存吗?

概述

不可以去掉第二级缓存。

原因

第二级缓存可以解决实例被重复代理的问题。(同时,也是设计上的需要)

详解

假如有这种情况:A同时依赖于B和C,B和C都依赖于A。即:

  1. A实例化时,先提前暴露objectFactoryA到第三级缓存,调用getBean(B)依赖注入B实例。
  2. B实例化,实例化之后,提前暴露objectFactoryB到第三级缓存,调用getBean(A)依赖注入A实例,由于提前暴露了objectFactoryA,此时从第三级缓存中获取到A实例,并将A实例放到第二级缓存。B实例完成了依赖注入,升级为一级缓存。
  3. A实例化开始填充属性,调用getBean(C)注入C实例
  4. C实例化之后,提前暴露objectFactoryC到第三级缓存,调用getBean(A)依赖注入A实例。由于第2步将A实例放到了第二级缓存,此时可以从第二级缓存中获取到A实例

 那么,如果没有第二级缓存,会如何处理呢?

  • 法1:从第三级获取到实例后,放到第一级缓存
    • 存在的问题:按照设计思路,第一级存放的是初始化好的对象,直接放到第一级缓存是不合适的。
  • 法2:从第三级获取到实例后,不处理,下次依然从第三级缓存获取
    • 如果有其他实例依赖了这个对象,会再次对这个对象进行代理,这就导致了重复代理。
4

评论3

请先

  1. 从上面的过程中我们可以得到一个非常非常重要的结论 当某个 bean 进入到 2 级缓存的时候,说明这个 bean 的早期对象被其他 bean 注入了,也就是说,这个 bean 还是半成品,还未完全创建好的时候,已经被别人拿去使用了,所以必须要有 3 级缓存,2 级缓存中存放的是早期的被别人使用的对象,如果没有 2 级缓存,是无法判断这个对象在创建的过程中,是否被别人拿去使用了。 3 级缓存是为了解决一个非常重要的问题:早期被别人拿去使用的 bean 和最终成型的 bean 是否是一个 bean,如果不是同一个,则会产生异常,所以以后面试的时候被问到为什么需要用到 3 级缓存的时候,你只需要这么回答就可以了:三级缓存是为了判断循环依赖的时候,早期暴露出去已经被别人使用的 bean 和最终的 bean 是否是同一个 bean,如果不是同一个则弹出异常,如果早期的对象没有被其他 bean 使用,而后期被修改了,不会产生异常,如果没有三级缓存,是无法判断是否有循环依赖,且早期的 bean 被循环依赖中的 bean 使用了。。 spring 容器默认是不允许早期暴露给别人的 bean 和最终的 bean 不一致的,但是这个配置可以修改,而修改之后存在很大的分享,所以不要去改,通过下面这个变量控制
    夜空中最亮的星 2024-04-29 1
    • 写得很好 :redface: ,还有后续吗,博主的好长
      流年 2024-05-25 0
      • Spring系列还会更新基础和进阶技术。
        自学精灵 2024-05-26 0
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录