为什么Spring需要三级缓存解决循环依赖,而不是二级缓存?

d836db8a2c6daaf92c99de559b57c03d.png

来源:https://www.cnblogs.com/semi-sub/p/13548479.html

在使用spring框架的日常开发中,bean之间的循环依赖太频繁了,spring已经帮我们去解决循环依赖问题,对我们开发者来说是无感知的,下面具体分析一下spring是如何解决bean之间循环依赖,为什么要使用到三级缓存,而不是二级缓存?

bean生命周期

首先大家需要了解一下bean在spring中的生命周期,bean在spring的加载流程,才能够更加清晰知道spring是如何解决循环依赖的

93ff50f0d89db99bcf64017358011dc1.png

我们在spring的BeanFactory工厂列举了很多接口,代表着bean的生命周期,我们主要记住的是我圈红线圈出来的接口, 再结合spring的源码来看这些接口主要是在哪里调用的

efe9d929f2cda333cd43897ed7b6dd8a.png

AbstractAutowireCapableBeanFactory类的doCreateBean方法是创建bean的开始,我们可以看到首先需要实例化这个bean,也就是在堆中开辟一块内存空间给这个对象,createBeanInstance方法里面逻辑大概就是采用反射生成实例对象,进行到这里表示对象还并未进行属性的填充,也就是@Autowired注解的属性还未得到注入

333012a13c61cd4ec91a17514bf22224.png

我们可以看到第二步就是填充bean的成员属性,populateBean方法里面的逻辑大致就是对使用到了注入属性的注解就会进行注入,如果在注入的过程发现注入的对象还没生成,则会跑去生产要注入的对象,第三步就是调用initializeBean方法初始化bean,也就是调用我们上述所提到的接口

23b644b74c12529bdfda0bcaf7df7816.png

可以看到initializeBean方法中,首先调用的是使用的Aware接口的方法,我们具体看一下invokeAwareMethods方法中会调用Aware接口的那些方法

02576078c0f790f66fdcbbfe459b6e26.png

我们可以知道如果我们实现了BeanNameAware,BeanClassLoaderAware,BeanFactoryAware三个Aware接口的话,会依次调用setBeanName(), setBeanClassLoader(), setBeanFactory()方法,再看applyBeanPostProcessorsBeforeInitialization源码

a5072c8c2c9ca37022cb25ec6340605c.png

发现会如果有类实现了BeanPostProcessor接口,就会执行postProcessBeforeInitialization方法,这里需要注意的是:如果多个类实现BeanPostProcessor接口,那么多个实现类都会执行postProcessBeforeInitialization方法,可以看到是for循环依次执行的,还有一个注意的点就是如果加载A类到spring容器中,A类也重写了BeanPostProcessor接口的postProcessBeforeInitialization方法,这时要注意A类的postProcessBeforeInitialization方法并不会得到执行,因为A类还未加载完成,还未完全放到spring的singletonObjects一级缓存中。

再看一个注意的点

a816d8f162178974975129016a13dbf2.png181a3713671a3193f440cf0937491a70.png

可以看到ApplicationContextAwareProcessor也实现了BeanPostProcessor接口,重写了postProcessBeforeInitialization方法,方法里面并调用了invokeAwareInterfaces方法,而invokeAwareInterfaces方法也写着如果实现了众多的Aware接口,则会依次执行相应的方法,值得注意的是ApplicationContextAware接口的setApplicationContext方法,再看一下invokeInitMethods源码

70f1f91d7f855542b88b6a2617cab2ec.png

发现如果实现了InitializingBean接口,重写了afterPropertiesSet方法,则会调用afterPropertiesSet方法,最后还会调用是否指定了init-method,可以通过标签,或者@Bean注解的initMethod指定,最后再看一张applyBeanPostProcessorsAfterInitialization源码图

453ae2fcd6dbc6b9d4e95effb1c73edc.png

发现跟之前的postProcessBeforeInitialization方法类似,也是循环遍历实现了BeanPostProcessor的接口实现类,执行postProcessAfterInitialization方法。整个bean的生命执行流程就如上面截图所示,哪个接口的方法在哪里被调用,方法的执行流程

最后,对bean的生命流程进行一个流程图的总结

ae7d44dbb1670b437f149c9c9205a2c8.png

三级缓存解决循环依赖

上一小节对bean的生命周期做了一个整体的流程分析,对spring如何去解决循环依赖的很有帮助。前面我们分析到填充属性时,如果发现属性还未在spring中生成,则会跑去生成属性对象实例

c16fb5647985cc843eead2c895fb2b49.png

我们可以看到填充属性的时候,spring会提前将已经实例化的bean通过ObjectFactory半成品暴露出去,为什么称为半成品是因为这时候的bean对象实例化,但是未进行属性填充,是一个不完整的bean实例对象

4932f0ef97ca5c762b126a87ed5b29b6.png

spring利用singletonObjects, earlySingletonObjects, singletonFactories三级缓存去解决的,所说的缓存其实也就是三个Map

69625b8cf1a3586484e706343d016e92.png

可以看到三级缓存各自保存的对象,这里重点关注二级缓存earlySingletonObjects和三级缓存singletonFactory,一级缓存可以进行忽略。前面我们讲过先实例化的bean会通过ObjectFactory半成品提前暴露在三级缓存中

f87216ebb0309199b3ef741fbc33a837.png

singletonFactory是传入的一个匿名内部类,调用ObjectFactory.getObject()最终会调用getEarlyBeanReference方法。再来看看循环依赖中是怎么拿其它半成品的实例对象的。

我们假设现在有这样的场景AService依赖BService,BService依赖AService

  1. AService首先实例化,实例化通过ObjectFactory半成品暴露在三级缓存中

  2. 填充属性BService,发现BService还未进行过加载,就会先去加载BService

  3. 再加载BService的过程中,实例化,也通过ObjectFactory半成品暴露在三级缓存

  4. 填充属性AService的时候,这时候能够从三级缓存中拿到半成品的ObjectFactory

cb480ce6e2aa413f978c2249da2f69a6.png

拿到ObjectFactory对象后,调用ObjectFactory.getObject()方法最终会调用getEarlyBeanReference()方法,getEarlyBeanReference这个方法主要逻辑大概描述下如果bean被AOP切面代理则返回的是beanProxy对象,如果未被代理则返回的是原bean实例,这时我们会发现能够拿到bean实例(属性未填充),然后从三级缓存移除,放到二级缓存earlySingletonObjects中,而此时B注入的是一个半成品的实例A对象,不过随着B初始化完成后,A会继续进行后续的初始化操作,最终B会注入的是一个完整的A实例,因为在内存中它们是同一个对象。下面是重点,我们发现这个二级缓存好像显得有点多余,好像可以去掉,只需要一级和三级缓存也可以做到解决循环依赖的问题???

只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个bean没被AOP进行切面代理,如果这个bean被AOP进行了切面代理,那么只使用两个缓存是无法解决问题,下面来看一下bean被AOP进行了切面代理的场景

7bdab04f9c0b268271dd59242513a92d.png

我们发现AService的testAopProxy被AOP代理了,看看传入的匿名内部类的getEarlyBeanReference返回的是什么对象

9b7226efd49f306d4c22fb7adcf9ccf0.png

发现singletonFactory.getObject()返回的是一个AService的代理对象,还是被CGLIB代理的。再看一张再执行一遍singletonFactory.getObject()返回的是否是同一个AService的代理对象

6a84bb587f59e98c49afd3b548e52dad.png

我们会发现再执行一遍singleFactory.getObject()方法又是一个新的代理对象,这就会有问题了,因为AService是单例的,每次执行singleFactory.getObject()方法又会产生新的代理对象,假设这里只有一级和三级缓存的话,我每次从三级缓存中拿到singleFactory对象,执行getObject()方法又会产生新的代理对象,这是不行的,因为AService是单例的,所有这里我们要借助二级缓存来解决这个问题,将执行了singleFactory.getObject()产生的对象放到二级缓存中去,后面去二级缓存中拿,没必要再执行一遍singletonFactory.getObject()方法再产生一个新的代理对象,保证始终只有一个代理对象。还有一个注意的点

361feb3ad05912b59532c04b1720a221.png

既然singleFactory.getObject()返回的是代理对象,那么注入的也应该是代理对象,我们可以看到注入的确实是经过CGLIB代理的AService对象。所以如果没有AOP的话确实可以两级缓存就可以解决循环依赖的问题,如果加上AOP,两级缓存是无法解决的,不可能每次执行singleFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象

总结

前面先讲到bean的加载流程,了解了bean加载流程对spring如何解决循环依赖的问题很有帮助,后面再分析到spring为什么需要利用到三级缓存解决循环依赖问题,而不是二级缓存。网上可以试试AOP的情形,实践一下就能明白二级缓存为什么解决不了AOP代理的场景了

在工作中,一直认为编程代码不是最重要的,重要的是在工作中所养成的编程思维。

677d348fb1fff144a2042aee09d31d5b.gif

往期推荐

e83d889649a28671e62fc315183f80d8.png

SpringCloud Ribbon中的7种负载均衡策略!


0012e000193e58bf7e51b82e9c1f5057.png

SpringCloud Nacos + Ribbon 调用服务的 2 种方法!


d8cc5840ab54c8e4c5818bf61b2c499e.png

Spring Cloud Alibaba Nacos 的 2 种健康检查机制!


1ba80560377cbf8c9ba65c9316c3c8f1.gif

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

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

相关文章

20款华丽的几何形状字体【免费下载】

这里手机的字体使用几何形状设计。流畅简洁的线条,完美的圆形的角度建立一个完整性的设计感。使用几何形状生成出每一个优雅而现代的字母。这些字体可以用于标题和正文。由于他们的设计适合任何干净简约设计,因此很受欢迎。向下滚动并下载这些免费几何字…

MySQL 精选 60 道面试题(含答案)

金三银四到了,给大家整理一些数据库必知必会的面试题。基础相关1、关系型和非关系型数据库的区别?关系型数据库的优点容易理解,因为它采用了关系模型来组织数据。可以保持数据的一致性。数据更新的开销比较小。支持复杂查询(带 wh…

Spring Boot 如何解决多个定时任务阻塞问题?

大家好,我是不才磊哥~最近长文撸多了,有点累,今天来点简单的。今天这篇文章介绍一下Spring Boot 中 如何开启多线程定时任务?为什么Spring Boot 定时任务是单线程的?想要解释为什么,一定要从源码入手&#…

mysql之explain

⊙ 使用EXPLAIN语法检查查询执行计划 ◎ 查看索引的使用情况 ◎ 查看行扫描情况⊙ 避免使用SELECT * ◎ 这会导致表的全扫描 ◎ 网络带宽会被浪费话说工欲善其事,必先利其器。今天就简单介绍下EXPLAIN。 内容导航 idselect_typetabletypepossible_keyskeyke…

SpringCloud OpenFeign + Nacos正确打开方式!

作者 | 磊哥来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone)Nacos 支持两种 HTTP 服务请求,一个是 REST Template,另一个是 Feign Client。之前的文章咱们介绍过…

为什么创建线程池一定要用ThreadPoolExecutor?

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)在 Java 语言中,并发编程都是依靠线程池完成的,而线程池的创建方式又有很多,但从…

Objects.equals有坑

前言最近review别人代码的时候,发现有个同事,在某个业务场景下,使用Objects.equals方法判断两个值相等时,返回了跟预期不一致的结果,引起了我的兴趣。原本以为判断结果会返回true的,但实际上返回了false。记…

Java 18 正式发布,默认 UTF-8,finalize 被弃用,别再乱用了!

JDK 18 正式发布JDK 17 刚发布半年,JDK 18 又如期而至,JDK 版本号这算是成年了?JDK 18 发布了,栈长继续为大家解读!JDK 18 延续了 JDK 17 开创的免费策略,但,JDK 18~20 不是长期支持…

Spring官方推荐的@Transactional还能导致生产事故?

在Spring中进行事务管理非常简单,只需要在方法上加上注解Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。甚至很多人心里已经将Spring事务与Transactional划上了等号,只要有数据库相关操作就直接给方法加上Transactio…

京东二面:MySQL 主从延迟、读写分离 7 种解决方案!

我们都知道互联网数据有个特性,大部分场景都是 读多写少,比如:微博、微信、淘宝电商,按照 二八原则,读流量占比甚至能达到 90%结合这个特性,我们对底层的数据库架构也会做相应调整。采用 读写分离处理过程&…

再见Postman,这款API神器更好用!

代码未动,文档先行其实大家都知道 API 文档先行的重要性,但是在实践过程中往往会遇到很多困难。程序员最讨厌的两件事:1. 写文档,2. 别人不写文档。大多数开发人员不愿意写 API 文档的原因是写文档短期收益远低于付出的成本&#…

如何保证数据库和缓存双写一致性?

前言数据库和缓存(比如:redis)双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。我很负责的告诉大家,该问题无论在面试,还是工作中遇到的概率非…

面试官:AtomicInteger是如何保证线程安全?

blog.csdn.net/nanhuaibeian/article/details/120936139一、为什么引入 AtomicInteger ?谈到线程安全,会首先想到了synchronized 和 Lock,但是这种方式又有一个名字,叫做互斥锁,一次只能有一个持有锁的线程进入,再加上…

机器学习 训练验证测试_测试前验证| 机器学习

机器学习 训练验证测试In my previous article, we have discussed about the need to train and test our model and we wrote a code to split the given data into training and test sets. 在上一篇文章中,我们讨论了训练和测试模型的必要性,并编写了…

如何判断线程池已经执行完所有任务了?

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)很多场景下,我们需要等待线程池的所有任务都执行完,然后再进行下一步操作。对于线程 Thread …

IRCTC的完整形式是什么?

IRCTC:印度铁路餐饮和旅游公司 (IRCTC: Indian Railways Catering and Tourism Corporation) IRCTC is an abbreviation of Indian Railways Catering and Tourism Corporation. It is a subsidiary of the Indian Railway established by the Ministry of Railways…

分布式锁的 3 种实现方案!

前言 大家好,我是磊哥。今天跟大家探讨一下分布式锁的设计与实现。希望对大家有帮助,如果有不正确的地方,欢迎指出,一起学习,一起进步哈~分布式锁概述数据库分布式锁Redis分布式锁Zookeeper分布式锁三种分布式锁对比1.…

java学习笔记16--异常

java学习笔记16--异常 异常 异常时导致程序中断运行的一种指令流,如果不对异常进行正确的处理,则可能导致程序的中断执行,造成不必要的损失, 所以在程序的设计中必须要考虑各种异常的发生,并正确的做好相应的处理&am…

线程安全问题的 3 种解决方案!

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的…

一文读懂MySQL查询语句的执行过程

需要从数据库检索某些符合要求的数据,我们很容易写出 Select A B C FROM T WHERE ID XX 这样的SQL,那么当我们向数据库发送这样一个请求时,数据库到底做了什么?我们今天以MYSQL为例,揭示一下MySQL数据库的查询过程&a…