文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Spring为什么使用三级缓存而不是两级解决循环依赖问题?

2024-11-30 18:04

关注

Spring Bean相关的知识

Spring Bean 的创建过程

  1. 扫描xml或者注解获取BeanDefinition;
  2. 实例化bean:通过createBeanInstance方法创建bean的原始对象BeanWrapper;
  3. 注入bean的依赖:利用populateBean方法(本质是反射)注入bean的依赖属性;
  4. 初始化bean:调用initializeBean方法最终形成完整的bean对象;

Spring Bean 的三级缓存定义

三级缓存的查找策略是,先从一级缓存获取,若获取不到就从二级缓存,仍然获取不到则从三级缓存获取,若还是获取不到则通过bean对应的BeanDefinition信息实例化。

Tips:二、三级缓存会在DI的过程中被删除,最终所有的Bean都会变成完整的bean并存入一级缓存中。


private final Map> singletonFactories = new HashMap>(16);

private final Map earlySingletonObjects = new HashMap(16);

private final Map singletonObjects = new ConcurrentHashMap(256);

什么是循环依赖?

循环依赖是指:Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动完成后这俩个对象都必须是完整的bean。

循环依赖的场景有三种:

一个简单setter循环依赖的代码示例如下:

@Service
public class A {
// @Autowired也行
@Resource
private B b;
}

@Service
public class B {
// @Autowired也行
@Resource
private A a;
}

Spring 是如何利用多级缓存解决循环依赖的

我们先抛开Spring的实现来做一次解决循环依赖的设计推演。

在没有缓存的情况下循环依赖的场景

如图可以直接观察到,当没有缓存时,当发生循环依赖时直接死循环了,最终的结局就是StackOverflow或者OOM。

增加一层缓存

为了解决上面循环依赖的问题,我们加入一层缓存,缓存可以使用Map结构,key为beanName,value为对象实例。如下如:

从上图可以直观的看出,循环依赖的问题已经得到了完美解决,但是又有了一个新问题,这个缓存中的bean可能有已经创建完成的、正在创建中还没有注入依赖的,它们都掺杂在一起,我们如何保证Map里面的所有对象是完整的呢?一层缓存很显然不符合设计规范,也缺乏安全性与扩展性。

二级缓存设计

我们希望的是,明确已经构建完成的的Bean被放入到一个缓存中,创建中的bean在另外一个缓存中,于是就有了下面的结构:

与一级缓存架构设计的区别在于:

从目前看这个设计完美解决了bean的完整性问题,但是在实际生产中问题总是叠着问题,没有完美的架构设计。我们都知道Java中有代理,而且代理的应用非常广泛,包括在Spring中就有非常多的代理,那问题就来了,我们如何区分代理对象与普通对象?如果循环依赖中存在代理对象的循环依赖会发生什么呢?

代理对象的循环依赖

在现实开发过程中,我们往往会产生很多的代理对象,当存在代理对象加入到循环依赖流程会是什么样的场景,我们来推演一下,我们仍然使用二级缓存的设计来做推演。

如果我们在bean初始化完成之后再创建代理对象,整个流程是这样的:

从上图可以非常直观的看出,最终在一级缓存中的对象A是一个proxy_A,但是对象B依赖的对象A却是一个普通A!很明显现有的设计不能够满足代理对象的循环依赖问题。

如何解决这个问题呢?我们还是在上一个设计上做修改:

这个方案看起来解决了B对象依赖不到A的proxy对象问题,但是又引起了一个致命的问题,在A初始化完成之后还会创建一次代理对象,那么就创建了两次代理对象,他们是完全不一样的,这个代理对象不是单例的了!

解决代理对象的循环依赖之终极方案

解决代理对象的循环依赖之终极方案-三级缓存!

与二级缓存设计最大的不同点在于:

其它流程不在赘述,该设计中最重要的几个地方在Spring中的实现是更加细致的,我在流程途中只是简单概括,下面特殊说明一下几个点。

Spring Bean初始化会产生代理对象的场景

在上述流程中,标记位黄色的部分就是两个代理对象的创建的地方,在Spring中就是这两个后置处理器调用的地方,它们分别是:

在Spring中的第三级缓存有更加灵活设计

在Spring中,第三级缓存不仅仅是存入了实例化的对象,而是存入了一个匿名类ObjectFactory,getEarlyBeanReference函数的实现中会调用BeanPostProcessor执行用户自定义的逻辑。具体代码如下:

// 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.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
// addSingletonFactory方法是将bean加入三级缓存中
// 三级缓存会被ObjectFactory包装
// getEarlyBeanReference方法会执行Bean的后置处理器
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}

若初始化阶段的后置处理器对对象做了代理,Spring是如何处理的?

在Spring中若在initializeBean阶段的后置处理器对对象做了代理,那么Spring会对做依赖检查,具体代码如下:

if (earlySingletonExposure) {
// 这里我们还拿A、B两个对象举例
// 尝试从二级缓存中获取A,第二个参数是false表示不再从三级缓存获取(也就是执行ObjectFactory.getObject()
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
// exposedObject是执行initializeBean方法返回的A,可能是个proxy_A
// bean是首次实例化的A,若这两个对象不相等,说明initializeBean方法返回了代理对象,需要进行依赖检查
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
// 依赖检查逻辑
// 这一大段就是在检查,检查依赖了A对象的Bean集合
// 这里很好理解:例如B依赖了A,那么如果B没有创建好,那么我们把B从缓存删掉,在之后的构建中让其重新依赖A_proxy
// 若B已经创建好了,那么很不幸,只能报错了,因为B这时候依赖的是一个普通A,而不是proxy_A
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采用三级缓存设计?

我们再回到最初的问题上,其实上述整个设计推演过程就已经很好的回答了这个问题,这里再做一下补充。从上述Spring源码可知,其在第三级缓存中放入的是匿名类ObjectFactory,每次需要获取对象实例就会调用其getObject方法。我们举个例子:

假如现在没有earlySingletonObjects这一层缓存(也就是第二级缓存),也就是两级缓存结构,现在有三个对象,其依赖关系如下A->B、B->A和C、C->A,从这个依赖关系可以得出,A所在的ObjectFactory会被调用两次getObject(),如果两次都返回不同的proxy_A(毕竟后置处理器的代码是使用者自己写的,可能代码是new Proxy(A)),那么就可能导致,B、C对象依赖的proxy_A不是一个对象,那么这种设计是致命的。

这个案例也从侧面反映了三层缓存的设计必要性、必然性,也是为了让框架更加的灵活健壮,以上就是我对Spring bean 三层缓存设计的理解,如有疑问欢迎在评论区讨论留言。

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯