前言

在之前的内容中,我们简要探讨了循环依赖,并指出仅通过引入二级缓存即可解决此问题。然而,你可能会好奇为何在Spring框架中还需要引入三级缓存singletonFactories。在前述总结中,我已经提供了答案,即AOP代理对象。接下来,我们将深入探讨这一话题。

AOP

在Spring框架中,AOP的实现是通过一个名为BeanPostProcessor的类完成的,其中一个关键的BeanPostProcessor就是AnnotationAwareAspectJAutoProxyCreator。值得一提的是,该类的父类是AbstractAutoProxyCreator。在Spring的AOP机制中,通常会使用JDK动态代理或者CGLib动态代理来实现代理对象的生成。因此,如果在某个类的方法上设置了切面,那么最终这个类将需要生成一个代理对象来应用AOP的功能。

一般的执行流程通常是这样的:A类--->生成一个普通对象-->属性注入-->基于切面生成一个代理对象-->将该代理对象存入singletonObjects单例池中。

而AOP可以说是Spring框架中除了IOC之外的另一个重要功能,而循环依赖则属于IOC的范畴。因此,为了让这两个重要功能同时存在于Spring框架中,Spring需要进行特殊处理。

三级缓存

在处理这种情况时,Spring框架利用了第三级缓存singletonFactories。下面我们来看一下关于三级缓存的源代码实现:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
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) {
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;
}

首先,singletonFactories中存储的是某个beanName对应的ObjectFactory。在bean的生命周期中,生成完原始对象之后,Spring框架会构造一个ObjectFactory并将其存入singletonFactories中。这个ObjectFactory是一个函数式接口,因此支持Lambda表达式,形式为() -> getEarlyBeanReference(beanName, mbd, bean)。为了更清晰地理解这个过程,我提供一张图片。

getEarlyBeanReference

在上述Lambda表达式中,它实际上代表了一个ObjectFactory,执行该Lambda表达式将会调用getEarlyBeanReference方法。下面是getEarlyBeanReference方法的实现:

//AbstractAutowireCapableBeanFactory
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}

在整个Spring框架中,值得注意的是,只有AbstractAutoProxyCreator这个类在实现getEarlyBeanReference方法时才具有真正的意义。这个类专门用于处理AOP(面向切面编程)。

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

那么,getEarlyBeanReference方法的具体操作是什么呢?

首先,它会获取一个cachekey,这个cachekey实际上就是beanName。

接着,它会将beanName和bean(即原始对象)存储到earlyProxyReferences中。

接下来,它会调用wrapIfNecessary方法进行AOP操作,这将生成一个代理对象。

那么,什么时候会调用getEarlyBeanReference方法呢?让我们再次回到循环依赖的场景中。

在我上一节的基础上,我增加了两句话,以便更好地理解触发缓存机制以解决AOP代理对象生成的时机。

一旦原始对象通过构造方法生成后,会被存储到三级缓存中,并且会与一个lambda表达式关联。然而,在这个阶段,它并不会被执行。

一旦BBean需要ABean时,系统会首先查看三级缓存以确定是否存在缓存。如果存在缓存,则lambda表达式将会被执行,其代码已在前面展示过。该lambda表达式的目的是将代理对象放入earlySingletonObjects中。需要注意的是,此时代理对象并未被放入singletonObjects中。那么代理对象何时会被放入singletonObjects中呢?

这个时候你可能已经明白了earlySingletonObjects的用途。由于只获取了A原始对象的代理对象,这个代理对象并不完整,因为A原始对象尚未进行属性填充。因此,在这种情况下,我们不能直接将A的代理对象放入singletonObjects中。因此,我们只能将代理对象放入earlySingletonObjects,这样依次类推。

在Spring框架中,在循环依赖场景下,当Bean B创建完成后,Bean A继续其生命周期。在Bean A完成属性注入后,根据其自身逻辑进行AOP操作。此时,我们知道Bean A的原始对象已经经历了AOP处理,因此对于Bean A本身而言,不需要再次进行AOP。那么,如何确定一个对象是否已经经历了AOP呢?

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

没错,这个earlyProxyReferences确实提前缓存了对象是否已经被代理过,这样就避免了重复的AOP处理。

举例反证

那么问题来了,当A对象创建时,它可以是原始对象,但当B对象创建时,却成功创建了A的代理对象。然后再回头给A对象进行属性注入和初始化,这些操作似乎与代理对象无关?

这个问题涉及到了 Spring 中动态代理的实现。无论是使用cglib代理还是jdk动态代理生成的代理类,代理时都会将目标对象 target 保存在最终生成的代理 $proxy 中。你可以将代理对象看作是对原始对象地址的一层包装,最终仍然会回到原始对象上。因此,对原始bean的进一步完善实际上也就是对代理对象的完善。

还有一个需要注意的问题,当A创建时,由于earlyProxyReferences缓存的原因,并没有创建代理对象,因此此时A仍然保持为原始对象。我们知道,当bean创建完成后,它将被放入一级缓存中,但如果在此之后被其他对象引用,那不就会出现问题吗?别人引用的都是原始对象了,而不是代理对象,但是请不要着急,因为在实例化之后,有一行代码可以解决这个问题。

if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
......省略代码

在实例化原始对象后,他会首先从三级缓存中检查是否存在缓存对象。这是因为在创建B对象时,已经将A的代理对象放入二级缓存。因此,取出的对象是代理对象。接着,当进行 exposedObject == bean 的比较时,发现它们不相同。因此,以代理对象为准并将其返回。最终,最外层存储的将是代理对象。

protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}

总结

在上一个章节中我们提到了今天要讨论三级缓存,让我们根据上面提到的三级缓存内容,做一个详尽的总结:

  • singletonObjects:缓存经过了完整生命周期的bean。

  • earlySingletonObjects缓存了未经过完整生命周期的bean。当某个bean出现循环依赖时,该bean会被提前放入earlySingletonObjects中。如果该bean需要经过AOP,那么代理对象将会被放入earlySingletonObjects;否则,原始对象将被放入其中。然而,无论是代理对象还是原始对象,它们的生命周期都尚未完全结束。因此视为未经过完整生命周期的bean。

  • singletonFactories缓存的是一个ObjectFactory,这个ObjectFactory实际上是一个Lambda表达式。在每个Bean的生成过程中,当原始对象实例化完成后,会提前基于原始对象生成一个Lambda表达式,并将其保存到三级缓存中。这个Lambda表达式可能会被使用,也可能不会。如果当前Bean不存在循环依赖,那么这个Lambda表达式将不会被使用,当前的Bean将按照正常的生命周期执行完毕,并将自身放入singletonObjects中。但是,如果在依赖注入的过程中发现了循环依赖(即当前正在创建的Bean被其他Bean所依赖),则会从三级缓存中取出Lambda表达式,并执行它以获取一个对象,然后将得到的对象放入二级缓存中。需要特别注意的是,如果当前Bean需要AOP处理,则执行Lambda表达式后得到的将是代理对象;否则,直接得到的是原始对象。

当涉及Spring框架中动态代理的实现机制时,除了已经提到的earlySingletonObjects和singletonFactories这两个缓存外,还有一个重要的缓存值得一提,那就是earlyProxyReferences。这个缓存的作用在于记录某个原始对象是否已经进行过AOP(面向切面编程)处理。

至此,整个循环依赖解决完毕。

解析Spring中的循环依赖问题:再探三级缓存(AOP)的更多相关文章

  1. 面试必杀技,讲一讲Spring中的循环依赖

    本系列文章: 听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configu ...

  2. 面试阿里,腾讯,字节跳动90%都会被问到的Spring中的循环依赖

    前言 Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中比较高阶的问题,那么循环依赖必定逃 ...

  3. 【Spring】Spring中的循环依赖及解决

    什么是循环依赖? 就是A对象依赖了B对象,B对象依赖了A对象. 比如: // A依赖了B class A{ public B b; } // B依赖了A class B{ public A a; } ...

  4. 从一部电影史上的趣事了解 Spring 中的循环依赖问题

    title: 从一部电影史上的趣事了解 Spring 中的循环依赖问题 date: 2021-03-10 updated: 2021-03-10 categories: Spring tags: Sp ...

  5. Spring中的循环依赖解决详解

    前言 说起Spring中循环依赖的解决办法,相信很多园友们都或多或少的知道一些,但当真的要详细说明的时候,可能又没法一下将它讲清楚.本文就试着尽自己所能,对此做出一个较详细的解读.另,需注意一点,下文 ...

  6. 一起来踩踩 Spring 中这个循环依赖的坑

    1. 前言 2. 典型场景 3. 什么是依赖 4. 什么是依赖调解 5. 为什么要依赖注入 6. Spring的依赖注入模型 7. 非典型问题 参考资料 1. 前言 这两天工作遇到了一个挺有意思的Sp ...

  7. Spring中的循环依赖

    循环依赖 在使用Spring时,如果主要采用基于构造器的依赖注入方式,则可能会遇到循环依赖的情况,简而言之就是Bean A的构造器依赖于Bean B,Bean B的构造器又依赖于Bean A.在这种情 ...

  8. Spring中解决循环依赖报错的问题

    什么是循环依赖 当一个ClassA依赖于ClassB,然后ClassB又反过来依赖ClassA,这就形成了一个循环依赖: ClassA -> ClassB -> ClassA 原创声明 本 ...

  9. Spring源码-循环依赖源码解读

    Spring源码-循环依赖源码解读 笔者最近无论是看书还是从网上找资料,都没发现对Spring源码是怎么解决循环依赖这一问题的详解,大家都是解释了Spring解决循环依赖的想法(有的解释也不准确,在& ...

  10. Spring 如何解决循环依赖问题?

    在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的. 这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能 ...

随机推荐

  1. 将字符串变成数组split

    字符串变成数组,常用来获取数组中我们需要的值. var str="http://op/adfie/life.png"; let arr=str.split('.'); consol ...

  2. 【解决一个小问题】macbook m2 上交叉编译 gozstd

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 已知 zstd 是一个优秀的压缩库,gozstd封装了这个 ...

  3. 【深度学习项目五】:利用LSTM网络进行情感分析(NLP)

    相关文章: [深度学习项目一]全连接神经网络实现mnist数字识别](https://blog.csdn.net/sinat_39620217/article/details/116749255?sp ...

  4. C/C++ Npcap包实现ARP欺骗

    npcap 是Nmap自带的一个数据包处理工具,Nmap底层就是使用这个包进行收发包的,该库,是可以进行二次开发的,不过使用C语言开发费劲,在进行渗透任务时,还是使用Python构建数据包高效,唯一的 ...

  5. 多进程实现socket通信(Python)

    服务器端: #__author__:Kelvin #date:2020/5/9 11:35 import socket from multiprocessing import Process def ...

  6. 创建framework静态库和.a静态库

    在APP项目中使用的静态库有两种,一是.a静态库,另一种是framework静态库.下面分布介绍这2中静态库的创建过程,以及通过脚本工具做自动化打包的2种方式.   Framework静态库生成 如果 ...

  7. 三星发布990 EVO SSD:同时支持PCIe 4.0和PCIe 5.0

    1月8日消息,三星发布了新款产品--990 EVO SSD,这是首款同时支持了PCIe 4.0 x4及PCIe 5.0 x2通道的SSD. 据了解,990 EVO面向中端市场,为2280 M.2规格, ...

  8. 从TF-IDF 到BM25, BM25+,一文彻底理解文本相关度

    相关性描述的是⼀个⽂档和查询语句匹配的程度.我们从搜索引擎召回时,肯定希望召回相关性高的数据,那么如何来量化相关度呢. 首先,我们定义,一个文档doc,由多个词语 term 组成. 最早,通过最简单的 ...

  9. CF1859

    A 让 \(c\) 保存数组中所有最大的数,如果所有数都相等则 \(-1\). B 只需要记录每个序列的最小值和次小值,然后对次小值求前后缀和. C 枚举最大值 \(mx\),然后遍历 \(i:n\s ...

  10. 【若归】 【LGR-142-Div.4】洛谷入门赛 #13赛后反思

    比赛链接:[LGR-142-Div.4]洛谷入门赛 #13 rk288,比前几次差(可能是因为rated?) A 十年OI一场空,不开long long见祖宗 #include<bits/std ...