Spring循环引用和三级缓存

news2024/11/23 15:37:38

前言

Spring 解决 Bean 之间的循环引用关系用到了三级缓存,那么问题来了。三级缓存是怎么用的?每一层的作用是什么?非得用三级吗?两级缓存行不行?

理解循环引用

所谓的“循环引用”是指 Bean 之间的依赖关系形成了一个循环,例如 a 依赖 b,b 又依赖 a。

@Component
public class A {
  @Autowired
  B b;
}

@Component
public class A {
  @Autowired
  B b;
}

开发者在设计阶段,应该尽量避免出现循环引用,因为这种依赖关系本身就违反了“单一职责”的设计原则,增加了代码的复杂性和维护成本。
抛开设计原则不谈,循环引用给 Spring 带来了依赖注入的问题。在这个例子中,Spring 实例化 a 时发现要为其注入 b,于是赶忙去实例化 b,实例化 b 的过程中又发现要为其注入 a,但此时 a 还没创建完,陷入一个死循环,就像死锁一样。
为什么多级缓存可以解决循环引用的问题呢?

理解多级缓存

Spring 里的三级缓存其实就是三个 Map 容器,先抛开 Spring 不谈,如果让我们设计一个多级缓存方案来解决循环引用的问题,我们会怎么做呢?

一级缓存

一级缓存的作用是为了保证单例。在 Spring 里面,Bean 默认是单例的,多次调用getBean() 得到的是同一个实例。基于这个规则,我们可以用一级缓存来实现一个保证单例的 IOC 容器。

public class Ioc1 {

    private final Map<Class, Object> singletonObjects = new ConcurrentHashMap<>();

    public synchronized <T> T getBean(Class<T> clazz) {
        Object bean = singletonObjects.get(clazz);
        if (bean == null) {
            singletonObjects.put(clazz, bean = createBean(clazz));
        }
        return (T) bean;
    }

    @SneakyThrows
    private <T> Object createBean(Class<T> clazz) {
        T bean = clazz.newInstance();
        return bean;
    }
}

二级缓存

一级缓存只能保证单例,对于循环引用是没办法解决的。因此,我们可以再加一级缓存,引入二级缓存来解决循环引用问题。于是,我们实现了第二版 IOC 容器,核心思路是把未被初始化的 bean 提前暴露到二级缓存,依赖注入时允许注入半成品 bean

public class Ioc2 {
    private final Map<Class, Object> singletonObjects = new ConcurrentHashMap<>();
    private final Map<Class, Object> earlySingletonObjects = new ConcurrentHashMap<>();

    public synchronized <T> T getBean(Class<T> clazz) {
        Object bean = singletonObjects.get(clazz);
        if (bean == null) {
            bean = earlySingletonObjects.get(clazz);
            if (bean == null) {
                singletonObjects.put(clazz, bean = createBean(clazz));
            }
        }
        return (T) bean;
    }

    @SneakyThrows
    private <T> Object createBean(Class<T> clazz) {
        T bean = clazz.newInstance();
        // 把未被初始化的bean提前暴露到二级缓存
        earlySingletonObjects.put(clazz, bean);
        populateBean(bean);
        return bean;
    }

    // 属性注入
    @SneakyThrows
    private <T> void populateBean(T bean) {
        for (Field field : bean.getClass().getDeclaredFields()) {
            Object fieldValue = getBean(field.getType());
            field.setAccessible(true);
            field.set(bean, fieldValue);
        }
    }
}

三级缓存

大多数情况下,二级缓存已经够用了,但是 Spring 还有一项强大的功能:基于 Bean 生成代理对象做增强。
此时,用二级缓存就面临一个问题:代理对象何时生成?

  • 如果等 Bean 初始化后再生成 Proxy,那已经被注入的属性却是个未被代理 Bean,这显然是不能接受的
  • 如果 Bean 初始化前就提前生成代理对象,这样能保证注入的属性也是被代理的 Bean,但是这不符合 Spring 的设计原则

为什么提前生成代理对象不符合 Spring 的设计原则呢???
因为在 Spring 的 Bean 和 AOP 的生命周期里,应该是先实例化并初始化 Bean 以后,再调用 BeanPostProcessor 的子类 AbstractAutoProxyCreator 的后处理器方法来生成代理对象。提前基于未被初始化的半成品 Bean 生成代理对象,这一点违背了 Spring 的设计原则,后处理器理应认为要扩展的 Bean 是一个完整的 Bean,万一需要访问其属性,拿到的却是 null,可能导致程序错误。

所以,我们可以得出一个结论:Spring 循环引用的问题,只用二级缓存是完全没问题的,前提是要提前生成代理 Bean 并暴露到二级缓存。功能是没问题,但是这一点违背了 Spring 的设计原则,所以 Spring 要尽量避免这个问题,才引入的三级缓存。

Spring 给出的方案是:引入一个三级缓存,尽可能避免提前创建代理对象,万一真的发生了循环引用,不得已而为之,也只能提前生成了。
所以,我们现在可以给出一个最终版的 IOC 实现了,逻辑基本和 Spring 一致。核心思路是,Bean 实例化以后,提前暴露一个 ObjectFactory 到三级缓存,如果没有循环引用,代理对象不会提前创建,Bean 的生命周期保持一致。如果发生了循环引用,就只能提前创建代理对象,并把它从三级缓存挪到二级缓存,避免重复创建,其它 Bean 注入的也是被代理后的 Bean。

public class Ioc3 {
    private final Map<Class, Object> singletonObjects = new ConcurrentHashMap<>();
    private final Map<Class, Object> earlySingletonObjects = new ConcurrentHashMap<>();
    private final Map<Class, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>();
    // 代理对象缓存
    private final Map<Class, Object> proxyCache = new ConcurrentHashMap<>();

    public synchronized <T> T getBean(Class<T> clazz) {
        Object bean = singletonObjects.get(clazz);
        if (bean == null) {
            bean = earlySingletonObjects.get(clazz);
            if (bean == null) {
                ObjectFactory<?> objectFactory = singletonFactories.get(clazz);
                if (objectFactory != null) {
                    bean = objectFactory.getObject();
                    earlySingletonObjects.put(clazz, bean);
                    singletonFactories.remove(clazz);
                } else {
                    bean = createBean(clazz);
                    singletonObjects.put(clazz, bean);
                }
            }
        }
        return (T) bean;
    }

    @SneakyThrows
    private <T> Object createBean(Class<T> clazz) {
        T bean = clazz.newInstance();
        singletonFactories.put(clazz, () -> wrapIfNecessary(bean));
        populateBean(bean);
        return wrapIfNecessary(bean);
    }

    private <T> T wrapIfNecessary(final T bean) {
        if (true) {
            Object proxy = proxyCache.get(bean.getClass());
            if (proxy == null) {
                proxy = ProxyFactory.getProxy(bean.getClass(), new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                        if (method.getDeclaringClass().isAssignableFrom(Object.class)) {
                            return method.invoke(this, args);
                        }
                        System.err.println("before...");
                        Object result = method.invoke(bean, args);
                        System.err.println("after...");
                        return result;
                    }
                });
                proxyCache.put(bean.getClass(), proxy);
            }
            return (T) proxy;
        }
        return bean;
    }

    @SneakyThrows
    private <T> void populateBean(T bean) {
        for (Field field : bean.getClass().getDeclaredFields()) {
            Object fieldValue = getBean(field.getType());
            field.setAccessible(true);
            field.set(bean, fieldValue);
        }
    }
}

Spring三级缓存实现

Spring 三级缓存对应的 Map 声明在 DefaultSingletonBeanRegistry 类,如下:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

一级缓存 singletonObjects:存放完整的,初始化后的 Bean
二级缓存 singletonFactories:存放未被初始化的半成品 Bean
三级缓存 earlySingletonObjects:存放 Bean 对应的 ObjectFactory,用于提前生成代理对象

在获取单例 Bean 时,Spring 会先查找一级和二级缓存,都没有时再查找三级缓存,如果找到了,就提前创建代理对象并返回。

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	// 查一级缓存
	Object singletonObject = this.singletonObjects.get(beanName);
	// 没有且Bean在创建中
	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
		// 查二级缓存取
		singletonObject = this.earlySingletonObjects.get(beanName);
		// 没有且允许引用半成品Bean
		if (singletonObject == null && allowEarlyReference) {
			synchronized (this.singletonObjects) {
				// double check
				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;
}

如果不存在循环引用,ObjectFactory#getObject 就不会被调用,也就不会提前创建代理对象。
doCreateBean() 方法里,会提前把未被初始化的半成品 Bean 封装成 ObjectFactory 暴露到三级缓存:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
  ......
  boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
			isSingletonCurrentlyInCreation(beanName));
	if (earlySingletonExposure) {
		addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
	}
	......
}

getObject() 方法也就是getEarlyBeanReference() ,它会调用后处理器SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference 生成代理对象(未必所有的Bean都需要被代理),目前只有一个实现类 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);
}

尾巴

Spring 为了解决循环引用的问题,设计了三级缓存。一级缓存的作用是保证单例;二级缓存的作用是解决循环引用;三级缓存是为了尽量避免基于半成品 Bean 提前创建代理对象。单从功能上说,只用二级缓存完全没问题,前提是被代理的 Bean 都要提前创建代理对象,这一点违背了 Spring 的设计原则。在 Spring Bean 的生命周期里,应该是先实例化并初始化 Bean 后再通过后处理器进行扩展,对一个未被初始化的 Bean 做扩展,万一要访问其属性可能就会导致程序错误。Spring 的做法是提前暴露一个 ObjectFactory 对象,如果没有发生循环引用,其getObject() 就不会被调用,代理对象就不会提前生成,尽可能的保证 Bean 生命周期一致。
但是,如果真的发生了循环引用,且引用的 Bean 又是需要被代理的,也只能提前生成代理对象了。因为这么做违背了 Spring 的设计原则,所以新版本的 Spring 默认已经不允许循环引用了,必须手动开启。

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

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

相关文章

什么事“网络水军”?他们的违法活动主要有四种形式

我国治理网络水军&#xff0c;包括造谣引流、舆情敲诈、刷量控评、有偿删帖等各类“网络水军”等违法犯罪活动已经许久。 日前&#xff0c;官方召开新闻发布会&#xff0c;公布了相关的一些案件进程&#xff0c;今年已累计侦办相关案件339起&#xff0c;超过历年的全年侦办案件…

开启Android学习之旅-6-实战答题App

不经过实战&#xff0c;看再多理论&#xff0c;都是只放在笔记里&#xff0c;活学活用才是硬道理。同时开发应用需要循序渐进&#xff0c;一口气规划300个功能&#xff0c;400张表&#xff0c;会严重打击自己的自信。这里根据所学的&#xff0c;开发一个答题App。 题库需求分析…

Linux 系统之部署 ZFile 在线网盘服务

一、ZFile 介绍 1&#xff09;ZFile 简介 官网&#xff1a;https://www.zfile.vip/ GitHub&#xff1a;https://github.com/zfile-dev/zfile ZFile 是一款基于 Java 的在线网盘程序&#xff0c;支持对接 S3、OneDrive、SharePoint、又拍云、本地存储、FTP 等存储源&#xff0…

精彩推荐 |【Java技术专题】「重塑技术功底」攻破Java技术盲点之剖析动态代理的实现原理和开发指南(中)

攻破Java技术盲点之剖析动态代理的实现原理和开发指南 前提介绍技术回顾回顾问题分析代理对象实现了什么接口代理对象的方法体是什么 CGLIB动态代理CGLIB的原理继承方式 为什么要用CGLIB建立被代理的类cglib拦截器类测试类易错点&#xff1a;CGLIB的invoke和invokeSuper的区分i…

【2024最新-python3小白零基础入门】No1.python简介以及环境搭建

文章目录 一 python3 简介二 python语言的特点三 python安装四 安装开发工具-pycharm五 新建一个python项目1.新建项目2 配置虚拟环境3 运行项目 一 python3 简介 Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强的可读性&a…

系分笔记数据库技术之数据库安全措施

文章目录 1、概要2、数据库的保护措施3、数据库的故障4、备份和日志5、总结 1、概要 数据库设计是考试重点&#xff0c;常考和必考内容&#xff0c;本篇主要记录了知识点&#xff1a;数据库故障及解决、数据库安全保护措施和数据库备份及恢复。 2、数据库的保护措施 数据库安全…

学习笔记——C++二维数组

二维数组定义的四种方式&#xff1a; 1&#xff0c;数据类型 数组名[ 行数 ][ 列数 ]&#xff1b; 2&#xff0c;数据类型 数组名[ 行数 ][ 列数 ]{{数据1&#xff0c;数据2}&#xff0c;{数据3&#xff0c;数据4}}&#xff1b; 3&#xff0c;数据类型 数组名[ 行数…

LeetCode(242)有效的字母异位词⭐

给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 注意&#xff1a;若 s 和 t 中每个字符出现的次数都相同&#xff0c;则称 s 和 t 互为字母异位词。 示例 1: 输入: s "anagram", t "nagaram" 输出: true示例 2: 输…

2024年第九届图像、视觉与计算国际会议(ICIVC 2024)即将召开!

2024年第九届图像、视觉与计算国际会议&#xff08;ICIVC 2024&#xff09;将于2024年7月15-17日在中国苏州举行&#xff0c;本次会议是由昆山杜克大学和IEEE联合主办&#xff0c;SMC技术支持&#xff0c;西安科技大学&#xff0c;北京工业大学&#xff0c;中国海洋大学&#x…

5分钟彻底搞懂什么是token

大家好啊&#xff0c;我是董董灿。 几年前在一次工作中&#xff0c;第一次接触到自然语言处理模型 BERT。 当时在评估这个模型的性能时&#xff0c;领导说这个模型的性能需要达到了 200 token 每秒&#xff0c;虽然知道这是一个性能指标&#xff0c;但是对 token 这个概念却不…

Qt / day01

1. 思维导图 2. 自由发挥应用场景实现一个登录窗口界面。 代码(mywidget.cpp)&#xff1a; #include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(parent) {// windows setup //setup windows sizethis->resize(600, 370);//set window fixed si…

c++实现支持动态扩容的栈(stack)

1.在栈容量满时自动扩容: 支持自动扩容栈实现: // // myStack.hpp // algo_demo // // Created by Hacker X on 2024/1/9. //#ifndef myStack_hpp #define myStack_hpp #include <stdio.h> #include <string.h> //栈实现 //1.入栈 //2.出栈 //3.空栈 //4.满栈 …

图片纹理贴图

/* * 当需要给图形赋予真实颜色的时候&#xff0c;不太可能为没一个顶点指定一个颜色&#xff0c;通常会采用纹理贴图 * 每个顶点关联一个纹理坐标 (Texture Coordinate) 其它片段上进行片段插值 * */#include <iostream> #define STBI_NO_SIMD #define STB_IMAGE_IMPLE…

LeetCode刷题--- 下降路径最小和

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 个人专栏 力扣递归算法题 http://t.csdnimg.cn/yUl2I 【C】 ​​​​​​http://t.csdnimg.cn/6AbpV 数据结构与算法 ​​​http://t.csdnimg.cn/hKh2l 前言&#xff1a;这个专栏主要讲述动…

服务器迁移上云

一、服务器迁移上云 1、服务器迁移概念&#xff1a; 服务器迁移一般来说是将物理服务器从一个地点&#xff08;物理机房&#xff09;移动到另一个地点&#xff0c;或将数据从一台服务器移动到另一台服务器的过程。 物理服务器迁移场景&#xff1a; ● 机房搬迁&#xff1a;…

松鼠目标检测数据集VOC格式400张

松鼠&#xff0c;一种小巧玲珑、活泼可爱的啮齿类动物&#xff0c;以其蓬松的大尾巴和机敏的动作而广受欢迎。 松鼠通常体型小巧&#xff0c;四肢灵活&#xff0c;尾巴蓬松。它们的耳朵大而直立&#xff0c;眼睛明亮&#xff0c;给人留下了深刻的印象。松鼠的毛色因种类而异&a…

selenium点击链接下载文件,并获取文件

在自动化测试时&#xff0c;有时我们会需要自动化获取下载的文件&#xff0c;这是我们要怎么办呢&#xff0c;跟着我一步步的来获取下载的文件吧 首先声明下&#xff0c;我们需要引入的类 from selenium import webdriver from selenium.webdriver.chrome.options import Op…

练习-指针笔试题

目录 前言一、一维整型数组1.1 题目一1.2 题目二 二、二维整型数组2.1 题目一2.2 题目二2.3 题目三 三、结构体3.1 题目一&#xff08;32位机器运行&#xff09; 四、字符数组4.1 题目一4.2 题目二 总结 前言 本篇文章记录关于C语言指针笔试题的介绍。 一、一维整型数组 1.1 …

使用vue实现一个网页的贴边组件。

使用vue实现一个网页的贴边组件。 先来看效果&#xff1a; 2024-01-04 10.46.22 https://www.haolu.com/share/V00O6HWYR8/36207fc21c35b2a8e09bf22787a81527 下面是具体代码实现&#xff1a; 1、父组件。&#xff08;用于贴边展示的组件&#xff09; <template>&…

[NISACTF 2022]midlevel

[NISACTF 2022]midlevel wp 信息搜集 进入页面&#xff0c;右上角显示了我的真实 IP &#xff1a; 最下面提示&#xff1a;Build With Smarty ! &#xff1a; Smarty 是 PHP 的模板引擎&#xff0c;判断为 Smarty 模板注入。 Smarty 模板注入 推荐博客&#xff1a;Smarty…