Spring源码学习:三级缓存的必要性

news2024/12/24 17:04:04

目录

  • 前言
  • 概述
  • 正文
    • Spring的生命周期
    • Spring中循环依赖场景
    • Spirng中的三级缓存
    • Spring一级缓存解决循环依赖
    • Spring二级缓存解决循环依赖
    • Spring三级缓存解决循环依赖
  • 总结

前言

工作中可能会碰到循坏依赖问题,所以了解其Spring设计原理,对于解决问题更加高效。在之前的文章中讲解了Spring的代码过程。这篇文章讲解Spring中三级缓存的必要性。

概述

在这里插入图片描述Spring在属性注入时有两种方式,一种是实例化对象时同个构造器进行注入,这种情况如果产生循环依赖是没用办法解决的。另一种是实例化之后,来到属性填充流程,通过反射完成属性的注入,这种方式也会产生循环依赖问题,Spring中引用了三级缓存用于解决循环依赖。所以三级缓存只能解决特定场景下的循环依赖,一些场景是没办法解决的,如构造器注入、多例等。

正文

Spring的生命周期

在讲循环依赖之前,需要先了解一些Spring中Bean的生命周期,把其流程熟悉清楚才可以更好理解循环依赖场景。
在这里插入图片描述以上是Spring IOC大概流程图,我们可以看到Bean的生命周期为实例化、属性注入、初始化、销毁等过程。

Spring中循环依赖场景

  • 构造器循环依赖:Spring实例化过程中,会计算获取合适的构造函数,如果获取的构造器是一个有参的,此时的入参恰好也有该实例的场景,如实例化A时,构造器需要传入B,这时会先从三级缓存中获取;如果获取不到则会创建B,而创建B的过程中如果恰好使用的构造器是需要传入A的,这时尝试从缓存中获取,由于A还未被创建,所以此时获取不到,再去创建A就形成的闭环,陷入死循环了。如果创建B时,使用的是无参构造器,那么B是可以成功创建出来的,但是在B的属性填充环节又需要用到A,这也会产生循环依赖;构造器循环依赖时,无法解决;

  • 原型模式(多例)循环依赖:Spring在每次从获取Bean时,如果是多例则不会尝试从缓存中获取,每次都会创建一个新的Bean对象进行返回,而在创建时其属性中也有引用Bean本身的情况,如A中有属性B,其两者为原型模式。这样在创建A过程时会尝试获取B,由于是多例的,获取B时又会先创建A,这样就形成闭环,产生循环依赖问题;原型模式(多例)循环依赖时,无法解决。

  • DependsOn循环依赖:如果使用了DependsOn,则Spring在创建当前Bean之前,会先创建所依赖的Bean。如果依赖的Bean也依赖其本身或依赖的Bean的子孙们也依赖其本身也会产生循环依赖问题,如A依赖B,B依赖A;A依赖B,B依赖C,C依赖A;使用三级缓存可以解决此循环依赖问题。

  • 单例属性填充循环依赖:Spring在完成Bean的实例化之后,会调用populateBean方法,该方法中会对Bean属性进行注入,通过反射的方式使用set注入。这里并不是所有属性都会进行注入,只有进行配置的属性才会进行注入,如使用@Autowire会尝试从BeanFactory中进行获取注入。populateBean过程发生的问题跟上述发生的差不多,使用三级缓存可以解决此循环依赖问题。

  • 单例代理对象循环依赖:当我们使用@Async注解时,会生成一个新的代理对象。此时如果被代理类中属性类也引用了被代理类,则会出行循环依赖问题;使用三级缓存可以解决此循环依赖问题。

@Service
public class TestService1 {
 
    @Autowired
    private TestService2 testService2;
 
    public void test1() {
    }

@Service
public class TestService2 {
 
    @Autowired
    private TestService1 testService1;
 
    public void test2() {
    }

Spirng中的三级缓存

	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

该方法是Spring尝试从缓存中获取实例化后的对象过程,其中可以看到有三个缓存对象。

  • singletonObjects:一级缓存,存放成品对象,也就是完成了实例化、属性注入、初始化(如代理对象生成)等过程的完整对象;
  • earlySingletonObjects:二级缓存,存入实例化后还未进入属性注入,初始化方法调用的半成品对象;
  • singletonFactories:三级缓存,该缓存是存入ObjectFactory,并不是真正的bean对象,当通过beanName从三级缓存中获取了ObjectFactory的实现类之后,通过其getObject()方法获取真正的对象,返回的对象可能是代理后的对象,也可能是原生对象。

getSingleton方法首先会尝试从一级缓存中获取完整的对象,获取到则返回;如果获取不到则从二级缓存中获取,获取到则返回;如果二级缓存中也获取不到对象,则尝试从三级缓存中获取对象,此时获取到的对象是ObjectFactory对象,需要调用其getObject方法获取真正的对象,获取到之后存入二级缓存,移除三级缓存;下次获取可直接从二级缓存中获取。

Spring一级缓存解决循环依赖

当只有一级缓存时,发生了循环依赖。以A类有属性B,B类有属性A为例;

  1. 当实例化A之后,将A添加到一级缓存中,后进入属性注入流程;
  2. 在发现B需要被注入,此时会去一级缓存中查找B,查询不到则进行创建;
  3. 创建B之后,加入到一级缓存中。在进行属性注入时,发现需要A,此时会去一级缓存中查找A;
  4. 由于步骤1中一级缓存有A了,所以此时获取到A后进行属性注入,这时的B就是一个完整的对象。
  5. A获取到新创建的B之后,进行注入,此时A也是一个完整的对象了。

通过以上步骤,我们可以得出对于单例模式下,一级缓存是可以解决循环依赖的;但是这种方式是将半成品跟成品对象放在一块了,假设我们对SpringIOC进行拓展,实现新的上下文时,如果存在多线程的情况,当获取到一个半成品,调用其方法时,就会报空指针异常了,所以只使用一级缓存是很不友好的,拓展性太差了。

Spring二级缓存解决循环依赖

由于一级缓存的缺点,半成品跟成品Bean都混在一块,导致拓展性差的问题。使用二级缓存就可以解决一级缓存中的问题。

  1. 一级缓存存放完整对象、二级缓存存放半成品对象
  2. 我们在属性注入时,尝试获取所需的Bean,先从一级缓存中查找,查询不到再从二级缓存中进行查询。

通过二级缓存的方式,拓展性有所提升。但是二级缓存也是有所缺陷。

  1. A是一个需要被代理的对象,Spring IOC会先创建原始对象,在属性注入之后的初始化环节中才会对其进行代理。
  2. 创建普通的对象A之后,加入二级缓存中;属性注入时需要B,此时会创建B,B创建之后加入二级缓存。
  3. B属性注入时,需要用到A,此时会去二级缓存中查找A,找到后进行注入,并加入到一级缓存中,移除二级缓存。
  4. A获取到B之后,进行属性注入。注入完成后进入到初始化环节,调用BeanPostProcessor的postProcessAfterInitialization中生成代理对象EnhanceA;

大家是否发现B对象属性A是一个原生对象,并不是代理后的EnhanceA对象,所以这里就有问题了,二级缓存没办法解决代理的问题。

Spring三级缓存解决循环依赖

基于二级缓存的缺陷,这里Spring IOC引入了三级缓存。三级缓存存入的是一个FactoryObject,并不是真正的Bean对象。

public interface FactoryBean<T> {

	String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

	@Nullable
	T getObject() throws Exception;

	@Nullable
	Class<?> getObjectType();

	default boolean isSingleton() {
		return true;
	}

}

我们可以看到FactoryBean有getObject方法,在三级缓存中获取到FactoryBean类型对象后,通过调用其getObject方法返回真正的Bean对象。

  1. 创建A之后,创建一个匿名内部类的FactoryBean存入三级缓存中。
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		Object exposedObject = bean;
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		return exposedObject;
	}

从匿名内部类的代码实现,首先从BeanFactory工厂中获取所有的BeanPostProcessor ,如果SmartInstantiationAwareBeanPostProcessor 类型的则调用其getEarlyBeanReference方法。而是否会被代理,就看SmartInstantiationAwareBeanPostProcessor 中是否有加载代理生成的工具类(在Spring IOC的准备工作中创建的)。

  1. 创建B之后,加入三级缓存。从一级缓存中查询,查不到从二级缓存中查询,查不到从三级缓存中查询。查询到之后,根据beanName名称获取到FactoryBean对象,并调用getObject方法,最终会得到真正的bean对象。该bean对象可能是代理对象也可能不是,要看具体的配置。从三级缓存中获取到对象之后,将该对象加入到二级缓存中,移除三级缓存,因为每次调用三级缓存可能开销是很大的。

3.B 获取到代理对象A之后进行属性注入,此时B就是一个完整的对象了,将二级缓存、三级缓存中的半成品移除,并将完整的对象加入到一级缓存中。

4、A获取到创建的B进行属性注入后,加入一级缓存中,移除二级、三级缓存。

通过以上步骤我们可以看到就能够解决二级缓存的缺陷了;可能有读者会有疑惑:

为什么不直接创建代理对象之后直接放入二级缓存呢?

如果直接放入二级缓存中也是没有问题的,但是需要考虑到代理的创建过程是及其耗时的,所以这里相当于采用懒加载的方法,当需要用到时再去进行创建,避免容器启动时,大量对象的创建,导致内存泄露等问题。

三级缓存中调用了代理对象,在初始化流程中还会进行代理吗?

在第一次创建代理之后会放入代理缓存中,当下次进行代理时,会尝试从缓存中获取,获取到了直接返回即可;

总结

循环依赖的解决方式有多种,Spring IOC是一个框架,所在在设计时会考虑拓展性,性能等各种问题,所以也是其应用广泛的原因之一吧。

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

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

相关文章

c++ - 第26节 - c++知识梳理

1.STL知识梳理 STL知识掌握&#xff1a; 底层实现角度&#xff1a;六大组件 上层用的角度&#xff1a;容器、算法、迭代器 底层实现角度&#xff1a; 注&#xff1a; 1.可以认为迭代器是容器和算法的粘合剂&#xff0c;如果没有迭代器&#xff0c;那么算法要访问容器有两大问题…

三种循环的区别

三种循环的区别:1.for循环和while循环先判断条件是否成立&#xff0c;然后决定是否执行循环体&#xff08;先判断后执行)2.do..while循环先执行一次循环体&#xff0c;然后判断条件是否成立&#xff0c;是否继续执行循环体(先执行后判断)for和while的区别:1.条件控制语句所控制…

英语学习打卡day2

2023.1.20 1.if虚拟语气的倒装 If it were not for your help, I would be homeless. Were it not for your help, I would be homeless. 要不是你的帮助&#xff0c;我会无家可归。 2.plausible adj.似乎有理的;有道理的 plaus拍手&#xff0c;鼓掌 ible可…的- >能鼓掌的…

注册中心(一)

注册中心&#xff08;一&#xff09; 业务痛点 项目的架构从以前的单体结构发展到现在的微服务。不仅服务的数量变的多了&#xff0c;而且服务都是多节点的部署。 假如在serviceA去调用serviceB&#xff0c;当serviceA会在配置中配置一个serviceB的ip和port进行通信。 当se…

如何安装配置hbase

当完成hdfs、zookeeper的安装配置后&#xff0c;现在进入到hbase的安装和配置环节。这样的做的目的之一是要把海量的数据存入到hbase数据库中。JDK版本的要求hbase对JDK版本是有要求的&#xff0c;不是JDK版本越高越好&#xff0c;根据我走过的坑&#xff0c;目前最好的JDK版本…

LeetCode刷题复盘笔记—一文搞懂贪心算法之122. 买卖股票的最佳时机 II问题(贪心算法系列第三篇)

今日主要总结一下可以0贪心算法解决的一道题目&#xff0c;122. 买卖股票的最佳时机 II 题目&#xff1a;122. 买卖股票的最佳时机 II Leetcode题目地址 题目描述&#xff1a; 给你一个整数数组 prices &#xff0c;其中 prices[i] 表示某支股票第 i 天的价格。 在每一天&am…

深度学习实战 —— LSTM轨迹预测

前言 最近写了一份用LSTM之类的序列模型+SeNet预测轨迹,经过一系列调试效果最后比较理想了。记录下~ 上效果: 一些效果图 训练误差图: 测试误差图: 训练损失: 测试损失: batch平均耗时: 拟合效果:

(day7) 自学Java——面向对象进阶

目录 1.static静态变量 ​编辑 2.继承 ​编辑 3.多态 4.包、final、权限修饰符、代码块 5.抽象类 6.接口 7.内部类 非原创&#xff0c;为方便自己后期复习 1.static静态变量 静态存储位置的数据是共享的 练习&#xff1a;定义数组工具类 需求&#xff1a;在实际开发…

深度卷积对抗神经网络 基础 第二部分 DC-GANs

深度卷积对抗神经网络 基础 第二部分 DC-GANs DC-GANs &#xff08;DC-GANs Deep convolutional GAN&#xff09;是基于GANs的一种专门对图片生成的一种模型&#xff0c;其通过卷积操作来进行图片的一些基本操作来实现模型的功能。 激活函数 Activations 激活函数是任何输入…

代码随想录--哈希表章节总结

代码随想录–哈希表章节总结 1. LeetCode242 有效的字母异位词 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 注意&#xff1a;若 s 和 t 中每个字符出现的次数都相同&#xff0c;则称 s 和 t 互为字母异位词。 示例 1: 输入: s &quo…

高德地图红绿灯读秒是怎么实现的?(二)

通过上一篇高德官方回复&#xff0c;以及一些科技大佬们的脑回路&#xff0c;做了一些简单的回复&#xff1b; 这次好好的从个人研究观点来阐述一下这个论题 目前有两种说法&#xff0c;一种说是靠大数据分析&#xff0c;一种说是靠交管部门数据。 从个人的研究来看&#xff0…

socket 2---TCP编程

目录 一、TCP编程流程 二、函数接口 2.1、监听接口 2.2、发起连接 connect 2.3、接收新连接 accept 2.4、收发接口 三、代码实现 问题&#xff1a; 要是创建多个客户端的话会怎么样呢&#xff1f; 那么怎么去真正解决这个问题呢&#xff1f; 一、TCP编程流程 这里…

数据结构与算法基础(王卓)(9):线性表的应用(有序表合并)(有序,可重复)

PPT&#xff1a;第二章P176&#xff1b; 合并为一个新的整体&#xff1a;有序表的合并&#xff08;有序&#xff0c;可重复&#xff09; 线性表&#xff1a; 对于该操作的具体实现的流程设计&#xff1a;&#xff08;各模块&#xff09; 创建一个空表C 依次从A或B(中&#…

移动云国产商用密码规模化应用

前言 为深入贯彻落实《密码法》&#xff0c;推动商用密码技术在工业和信息化行业领域的融合应用&#xff0c;工业和信息化部密码应用研究中心组织开展了“首届全国商用密码应用优秀案例征集”工作&#xff0c;并评审选出15项优秀案例。 同时&#xff0c;为持久发挥本次活动的…

CSAPP笔记

目录 第一章 一个典型的硬件组成 从键盘上读取hello指令​编辑 存储器结构示例 相对性能公式 计算机系统抽象 第二章--信息的表示和处理 按位 & | ^ ~ 与逻辑运算 && || 逻辑右移和算术右移 左移 定义计算机如何编码和操作整数的数学定义 补码编码的定义 补码…

【JavaWeb】前端开发三剑客之CSS(上)

✨哈喽&#xff0c;进来的小伙伴们&#xff0c;你们好耶&#xff01;✨ &#x1f6f0;️&#x1f6f0;️系列专栏:【JavaWeb】 ✈️✈️本篇内容:CSS从零开始学习&#xff01; &#x1f680;&#x1f680;代码托管平台github&#xff1a;JavaWeb代码存放仓库&#xff01; ⛵⛵作…

擎创动态 | 官宣!与深智城集团正式签约

近日&#xff0c;上海擎创信息技术有限公司与深圳市智慧城市科技发展集团有限公司&#xff08;以下简称“深智城集团”&#xff09;就“一体化协同办公平台项目”达成战略合作&#xff0c;签约仪式已圆满完成。 ​深智城集团副总经理罗介平、智城软件公司常务副总经理韩小宇、智…

android 读取assets配置文件

方法1-getAssets().open(“re.properties”) try {Properties props new Properties();props.load(getAssets().open("re.properties"));Log.e(TAG, "className:" props.getProperty("className"));} catch (IOException e) {e.printStackTrace…

支持加密的日记应用程序DailyTxT

本文完成于 12 月下旬&#xff0c;对应的版本为 1.0.10(2022_11_02)&#xff1b; 什么是 DailyTxT &#xff1f; DailyTxT 是一个加密的 Web 日记应用程序&#xff0c;用于写下您当天的故事并轻松地再次找到它们。它是用 Python Flask&#xff08;后端&#xff09;和 Vue.JS&am…

23种设计模式(十二)——外观模式【接口隔离】

外观模式 文章目录 外观模式意图什么时候使用外观真实世界类比外观模式的实现外观模式的优缺点亦称:Facade 意图 外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容…