前言

在之前的内容中,我们简要探讨了循环依赖,并指出仅通过引入二级缓存即可解决此问题。然而,你可能会好奇为何在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. [转帖]Redis子进程开销与优化

    Redis子进程开销与优化 文章系转载,便于分类和归纳,源文地址:https://blog.csdn.net/y532798113/article/details/106870299 1.CPU 开销 ...

  2. [翻译]-hugePage的简要说明--部分内容

    hugePage的简要说明 本篇文档的主旨给linux内核支持的大页内存做一个简要的概述. 大页内存的实现是建立在大多数现代架构所都支持的多级页大小的特性之上的. 举例: x86架构下大部署CPU 的 ...

  3. pytest.ini配置文件

    pytest.ini文件是pytest框架独有的配置文件,主要作用就是在运行pytest.main时可指定运行顺序,也 就相当于在Terminal输入pytest+参数+路径效果一致,下面介绍几种简单 ...

  4. vue3中mixins的使用

    vue3-mixins 在开发的过程中我们会遇见相同或者相似的逻辑代码. 可以通过vue的 mixin 功能抽离公共的业务逻辑, 然后通过impor再组件中引入.通过mixins注册进来. 这样我们就 ...

  5. 正则表达式match方法和search方法

    正则表达式, //match() 方法可在字符串内检索指定的值 找到返回相关数据,找不到返回null var part = /Box/ig; var str = "this is box,i ...

  6. 【JS 逆向百例】反混淆入门,某鹏教育 JS 混淆还原

    关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后 ...

  7. MySQL【二】---数据库查询详细教程{查询、排序、聚合函数、分组}

    1.数据准备.基本的查询(回顾一下) 创建数据库 create database python_test charset=utf8; 查看数据库: show databases; 使用数据库: use ...

  8. spring boot jar混淆加密

    最近在做一个智能床垫的机构版项目,客户要求部署到客户那边要做代码混淆防止代码被反编译. 一:在需要加密的jar的pom.xml文件添加依赖 <!-- 设置 jitpack.io 仓库 --> ...

  9. 吉特日化MES & 日化制药工厂信息化系统架构图

    作者:情缘   出处:http://www.cnblogs.com/qingyuan/ 关于作者:从事仓库,生产软件方面的开发,在项目管理以及企业经营方面寻求发展之路 版权声明:本文版权归作者和博客园 ...

  10. Base64编码的优点与缺点

    Base64编码是一种将二进制数据转换为可打印ASCII字符的编码方式.它被广泛应用于数据传输和存储,以提升数据的可读性.可传输性和安全性. Base64编码解码 | 一个覆盖广泛主题工具的高效在线平 ...