【分析向】没有三级缓存会导致什么?

news2024/9/22 19:43:50

通过上篇(【实践向】当移除了三级缓存…… )的实践,我们得出的结论是:如果不存在代理对象,二级缓存就可以解决循环依赖性的问题,但是当存在代理对象的时候,二级缓存则无法完全解决循环依赖,需要引入三级缓存

那么、在没有三级缓存的情况下,引入代理后为什么会报错?

由于我们的代码只修改了两处,但前后出现了不同执行结果,所以我们可以将分析重点🔎放在修改的两处代码上(代码与文章【实践向】当移除了三级缓存…… 中的修改后的代码一致,想动手实践下的可以去瞅下,本文较长,这里就不贴叻)

给个“缩略图”吧(˶‾᷄ ⁻̫ ‾᷅˵)下面两张图中红框框内注释掉的为原代码,未注释的为修改后的代码

  • 第一处修改的位置

  • 第二处修改的位置

流程图

先放个报错的流程图,可以对照这个看后面的Debug步骤

Start^ ^!

ps >>> 自行尝试Bebug的客官需要注意的是我们这里只关注与beanA和beanB这两个bean相关的对象的相关操作,所以如果遇到Variable中的“name”或者“beanName”不是beanA/beanB的对象就直接单击Resume进入下一个断点即可(毕竟Spring不只需要创建这俩Bean)

启动后,获取BeanA

未获取到,开始创建BeanA的实例对象

我们直接到BeanA的实例化完成后,将其半成品BeanA@2289放入二级缓存的位置(本文中的半成品均指只完成实例化未完成初始化的实例对象)

上图可以看到此时半成品BeanA@2289的属性beanB还是null,接着开始进行属性填充,applyPropertyValues()方法中通过 valueResolver.resolveValueIfNecessary()方法解析出beanA的属性beanB并尝试从缓存中获取

显然还是获取不到的,于是开始创建BeanB的实例对象

实例化完成后生成了BeanB的半成品对象BeanB@2416,同样将其放入二级缓存中

然后进行属性填充,解析并尝试获取半成品对象BeanB@2416的属性beanA,此时二级缓存中已经有了BeanA的半成品对象BeanA@2289,于是获取到了对象BeanA@2289

通过调用setPropertyValues()方法将获取到的对象BeanA@2289填充到BeanB@2416对象的属性beanA中(如下图可以看到BeanB@2416的属性beanA已经是对象BeanA@2289了)

继续完成对象BeanB@2416的初始化

初始化过程中先是调用前置处理器

这里会遍历该工厂创建的bean的BeanPostProcessors,最终返回一个经过其所有BeanPostProcessor对象的后置处理器层层包装的对象

然后执行初始化方法,最后会调用后置处理器

与前置处理器逻辑基本一致

但是,有一个特别的BeanPostProcessor

此时会进入到AbstractAutoProxyCreator抽象类的postProcessAfterInitialization()方法中

在wrapIfNecessary()中为BeanB创建代理对象

将创建好的代理对象放入集合proxyTypes中,然后返回

返回到调用后置处理器的方法中,再继续遍历完剩余的后置处理器,对新生成的代理对象进行层层包装,最后将该代理对象返回

返回到初始化方法中

值得注意的,在applyBeanPostProcessorsAfterInitialization()方法的返回值赋值给wrapBean对象前,该对象的值是BeanB@2416

完成赋值后,wrapBean对象的值变成了BeanB@1a6d8329,没错就是刚创建的代理对象

接着完成了将bean对象注册到容器的操作,BeanB的实例对象的创建就完成了

还记得我们为啥么要创建BeanB的实例对象吗?

自问自答:因为在给BeanA的实例对象beanA的beanB属性赋值的时候没有从缓存中获取到BeanB的实例对象,于是我们开始了BeanB的实例对象的创建,所以当BeanB的实例对象创建好后,就会继续回到BeanA对象填充beanB属性的步骤,并将返回的BeanB@1a6d8329对象通过setPropertyValues()方法填充到了的beanB属性中

此时就完成了BeanA@2289对象的属性填充,开始执行初始化方法initializeBean()

根据上面执行的经验,我们直接进入到后置处理器的调用方法applyBeanPostProcessorsAfterInitialization()中

可以看到此时需要遍历的有4个BeanPostProcessor,当遍历到AspectJAwareAdvisorAutoProxyCreator时

进入postProcessAfterInitialization()方法,会发现再次来到了AbstractAutoProxyCreator抽象中

于是相同的流程,进入wrapIfNecessary()方法,一步一步创建BeanA的代理对象

创建完成后,将返回的代理对象BeanA@1c7696c6赋值给exposedObject对象,而这个对象原先的值是BeanA@2289

然后就要开始咱们的终极大判断叻(。・ω・。)ノ

获取缓存中名为beanA的对象并赋值给earlySingletonReference,此时earlySingletonReference是BeanA@2289,exposedObject在刚刚的操作被赋值为BeanA@1c7696c6,因此和bean(BeanA@2289)并不相等,于是程序会继续判断!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName),两个判断都满足,于是进入到else if的代码块中

在这段代码中获取到了beanA的依赖对象有beanB,并将其加入到集合actualDependentBeans中

End= =

因此此时!actualDependentBeans.isEmpty()的值为true,于是进入到了if条件代码块中,嘿嘿、然后就报错了(˶‾᷄ ⁻̫ ‾᷅˵)……

引发的问题

没有三级缓存引发了什么问题?

通过上面的Debug流程和报错信息不难发现,移除了三级缓存使得在容器中存在BeanA对象的实例,一个是已经注入到BeanB的半成品对象BeanA@2289,另一个是执行初始化时生成的代理对象BeanA@1c7696c6,一个单例模式下的容器怎么能容纳两个名为beanA对象,因而报错

简单来说,就是在生成代理对象的时候,发现已经有一个同名的对象存在并被其他对象依赖了

这个问题是在哪里解决的?

回答放前面(体贴下不想看过程的朋友(。・ω・。)ノ)

在上面测试流程中修改掉的两个方法getSingleton()和addSingletonFactory()中

其实无论是否配置了代理,二级缓存中都是存放半成品对象的,无需生成代理对象存放的就是一般对象的半成品,需要创建代理就存放的是代理对象的半成品

三级缓存的存在使得当判断需要生成代理对象时,会将原先生成的生成的代理对象

恢复原先的代码

再次启动,我们直接从BeanA的实例对象的创建开始,可以看到下图中执行到我们一开始修改代码用二级缓存替换三级缓存的位置,此时,二级缓存和三级缓存都还是空的(黄框框),并且已经创建了一个BeanA的半成品对象BeanA@2324

然后我们进入方法addSingletonFactory()中

执行完以后,就将beanA和其lambda表达式添加到了三级缓存中,此时二级缓存依旧是空的

随后将半成品对象BeanA@2324赋值给exposedObject,继续执行下面的逻辑

值得注意的是,这次,这个半成品对象并没有放进二级缓存中,那么意味着在后面创建BeanB的实例对象进行属性注入时,无法从二级缓存中获取到这个半成品对象,那么按照源码的逻辑,会继续尝试从三级缓存中获取,于是,会获取到在addSingletonFactory()方法中添加的beanA的lambda表达式,进一步创建出代理对象,为BeanB的实例对象的beanA属性赋值

我们继续Debug到创建BeanB的实例对象进行属性注入时获取BeanA的步骤

由于此时一级缓存、二级缓存都为空,因此会尝试从三级缓存中获取,此处会通过singletonFactory.getObject()这个回调方法去调用getEarlyBeanReference()方法(原因如下图),进而开始BeanA代理对象的创建

在刚进入这个方法时,可以看到bean对象和exposedObject对象的值是一致的,都是BeanA@2324

当这个方法中的逻辑都执行完成后,bean对象还是BeanA@2324,而exposedObject已经变成了代理对象BeanA@60099951

回调方法执行完后,将生成的代理对象返回赋值给变量singletonObject,接着将这个生成的代理对象添加进二级缓存,并从三级缓存中移除

使用获取到的代理对象完成属性赋值

继续完成BeanB实例对象的初始化,与BeanA的初始化流程一致,调用后置处理器的过程中同样会为BeanB生成代理对象,如下图中再在遍历调用AspectJAwareAdvisorAutoProxyCreator的postProcessAfterInitialization()初始化后置方法前,result=BeanB@2450

遍历结束后,返回的值是新生成的BeanB的代理对象BeanB@20140db9

在获取到完整的BeanB实例对象(代理对象)后,回到BeanA创建对象属性赋值的流程中,将获取到的代理对象BeanB@20140db9赋值给BeanA的beanB属性

继续执行BeanA的实例对象的初始化流程,来到调用后置处理器的步骤(嘿嘿( ̄∇ ̄)没错这里又要创建代理对象叻)

一切都是那么相似,又要调用AspectJAwareAdvisorAutoProxyCreator的postProcessAfterInitialization()初始化后置方法,来获取BeanA的代理对象,不同的是,此时二级缓存(earlySingletonObjects)中已经有了一个之前创建好的BeanA的代理对象BeanA@60099951,所以不会再创建了,直接返回传入的对象BeanA@2324

接下来的判断earlySingletonExposure为true,于是从缓存中获取到BeanA的代理对象,赋值给earlySingletonReference

由于初始化函数返回的exposedObject和bean对象相等,所以开始执行if条件代码块,即将earlySingletonReference的值赋值给exposedObject,并返回exposedObject

于是获取到了BeanA的代理对象BeanA@60099951

此时这个代理对象的属性beanB还是null

而通过beanA.getBeanB()获取的BeanB对象(进入了拦截器的逻辑),其beanA属性也是null(这跟没有引入代理的循环依赖也有区别)

当移除代理的相关配置后

还记得代理是在哪一个步骤替换掉同名的一般对象的吗?

自问自答:getEarlyBeanReference()

而当我们不引入代理时

此时,不会进入红框框内的判断逻辑代码块

返回的exposedObject对象就是传入的bean对象(都是BeanA@1513)

接着会将获取到的BeanA的半成品对象放入二级缓存,并将其工厂方法从三级缓存中移除

接着BeanB继续执行属性赋值、初始化,并在addSingleton()方法中,将生成的完整对象BeanB@1554放入一级缓存,将他的工厂方法从三级缓存中移除

可以看到初始化步骤中调用的后置处理器也从引入代理时的4个,减少到了3个,所以此处不会生成代理对象来覆盖已经生成的一般对象

此时全局只有一个BeanA的实例对象BeanA@1513

自然也没有机会报红框框中的异常(。・ω・。)ノ

一路返回,在addSingleton()方法中,将BeanA@1513也加入了一级缓存,并从二级三级缓存中把同名对象移除

于是一开始进入的getSingleton()方法获取了BeanA的完整的实例对象

于是循环依赖也解决了٩(˃̶͈̀௰˂̶͈́)و

一路返回到最初的getBean

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/181958.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器自动翻译古文拼音 - 十大宋词 - ALL

机器自动翻译古文拼音 - 十大宋词 - 雨霖铃寒蝉凄切 柳永https://mp.csdn.net/mp_blog/creation/editor/128779245机器自动翻译古文拼音 - 十大宋词 - 江城子乙卯正月二十日夜记梦 苏轼https://mp.csdn.net/mp_blog/creation/editor/128779156机器自动翻译古文拼音 - 十大宋词 …

0基础小白十分钟入门人工智能强化学习(附有实战源码)

强化学习概述 1.1 强化学习的学习任务目标 强化学习(Reinforcement Learning, RL),用官话讲,是机器学习的范式和方法论之一,用于描述和解决智能体(agent)在与环境的交互过程中通过学习策略以达成…

Mybatis-Plus 乐观锁与代码生成器

目录 乐观锁 问题引入 乐观锁实现思路 实现步骤 代码生成器 代码生成器分析 代码生成器实现 乐观锁 问题引入 业务并发现象带来的问题:秒杀 假如有100个商品或者票在出售,为了能保证每个商品或者票只能被一个人购买,如何保证不会出现超买或者重复…

记一次nginx崩溃事件

一、事件描述 2023年春节复工第一天,项目组同事反馈说业务系统中图像处理代理Nginx服务于1月23日发生崩溃,完成了重启操作,检查nginx的日志有如下报错: 2023/01/23 11:07:07 [crit] 3237#3237: *2253009 pwritev() "/var/c…

网络编程-----(Socket编程TCP)

在咱们的TCP API中,也是主要是涉及到两个类: 1)ServerSocket:主要是给TCP服务器来进行使用的; 2)Socket:我们既需要给客户端来进行使用,也需要给服务器来进行使用; 这样就是说我们是不需要使用专门的类来进行表示传输的包&#x…

Java学习之抽象模板模式

目录 一、基本介绍 二、模板设计模式能解决的问题 三、最佳实践 一、AA类 二、BB类 三、main方法实现 四、提取相同语句 五、建立继承关系 父类-Template 子类-AA类 子类-BB类 六、运行中的动态绑定机制 一、基本介绍 抽象类体现的就是一种模板模式的设计&#xff…

【Git】概述

目录 1.1 是什么 介绍 历史时间轴 版本控制工具 1.2 能干嘛 作用 Git工作机制 代码托管中心 集中式版本控制系统 分布式版本控制系统 1.3 去哪下 命令行工具:Git for windows 操作系统中可视化工具:TortoiseGit(了解) GitHub网站 1.1 是什…

带你走进Java8新特性Stream流的小世界

目录 一. 什么是流(Stream) 1.1 流的定义 1.2 流的特点 1.3 操作流 1.4 创建流 二. 流的中间操作 2.1 流的筛选与切片 2.1.1 filter 2.1.2 limit 2.1.3 skip 2.1.4 distinct 2.2 流的映射 2.2.1 map 2.2.2 flatMap 2.3 流的排序 2.3.1 s…

智公网:2023年教师编必背30考点

1、制度化教育阶段开始于:近代。 2、各国的学校教育系统基本形成于:19世纪末。 3、现在世界上大多数国家的义务教育年限在:9年或9年以上。 4、“不愤不启,不悱不发”启发教学法的最早倡导者是:孔子。 5、“建国君民…

【Spring】Spring 6 新特性一一HTTP Interface

简介 Spring 6 的第一个 GA 版本发布了,其中带来了一个新的特性——HTTP Interface。 这个新特性,可以让开发者将 HTTP 服务,定义成一个包含特定注解标记的方法的 Java 接口,然后通过对接口方法的调用,完成 HTTP 请求…

硬盘损坏数据恢复怎么操作?恢复数据的常用方法

硬盘一般固定在电脑里面的存储装置,里面保存着我们大量的数据。随着电脑的使用越加广泛,有时不免出现一些问题,比如硬盘在使用过程中出现数据错误,或者是硬盘的内部零件出现故障。出现这些问题,硬盘损坏数据恢复怎么操…

Redis实现UV统计 | 黑马点评

一、HyperLogLog 1、为什么用HyperLogLog 先介绍两个概念: UV:全称 Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人、1 天内同一个用户多次访问该网站,只记录 1 次。PV:全称 …

车载以太网 - SomeIP测试专栏 - 总纲

关于车载以太网中的SomeIP在网上也逐渐有越来越多的资料,讲的也是非常好;但是个人认为讲的泛,很难让初学者或者初入门者真正了解SomeIP到底是一个什么东西,以及它究竟在车载上有什么作用,本专栏会由浅入深的讲解SomeIP整个协议内容规范,并且对Tc8中SomeIP相关的协议测试用…

实习日记!

目录 http://localhost:5789实习第三天 接下来几天的target 实习第四天 Git的操作 实习第五天 12月5日-Mon 12月6日 12月9日 12月12日 12月15日 useState() hook 12月16日 useEffect() hook async 函数 异步编程 回调函数 12月17日 C#中的?&#x…

postgresql源码学习(54)—— HotStandby从库必须设置大于等于主库的参数

新的一篇本来计划研究lazy_scan_heap函数,但过于复杂还没研究出来… 下午做题遇到一个这样的问题,之前没太关注过,打算学习学习,避免主从配置踩坑。 题干搜一搜,没搜出啥有用的玩意…渣翻成英文搜一搜,搜出…

windows搭建go语言开发环境,IDEA安装go插件并运行Hello world代码

2023年1月27日1.Windows上安装Go语言开发包参考链接:http://c.biancheng.net/view/3992.html1.1.下载Go语言开发包可以在Go语言官网 (https://golang.google.cn/dl/) 下载Windows 系统下的Go语言开发包,如下图所示。这里我们下载的…

学习Docker就应该掌握的dockerfile语法与指令

在日常的工作中,常常需要制作自己的项目的镜像,一般通过以下两种方式制作镜像:Docker commit、Dockerfile。Docker commitDocker commit一般用做从一个运行状态的容器来创建一个新的镜像。定制镜像应该使用Dockerfile来完成。docker commit 容…

Git场景分析

当前的开发环境如下,我们每个人都对这个项目已经开发一段时间,接下来我们要切换成团队开发模式。 也就是我们由一个团队来完成这个项目实战的内容。团队有组长和若干组员组成(组长就是开发中的项目经理)。 练习场景如下: 1.由组长&#xff0…

LeetCode[1202]交换字符串中的元素

难度:中等题目:给你一个字符串 s,以及该字符串中的一些「索引对」数组 pairs,其中 pairs[i] [a, b]表示字符串中的两个索引(编号从 0 开始)。你可以 任意多次交换 在 pairs中任意一对索引处的字符。返回在…

java递归问题——汉诺塔

目录 🌲🌲什么是汉诺塔? 🐰 当只有1个圆盘的时候: 🐰 当只有2个圆盘的时候: 🐰 当只有3个圆盘的时候: 🌲🌲汉诺塔代码 🐰思路 &am…