Spring循环引用和三级缓存

前言

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.mfbz.cn/a/306159.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【算法刷题】Day28

文章目录 1. 买卖股票的最佳时机 III题干&#xff1a;算法原理&#xff1a;1. 状态表示&#xff1a;2. 状态转移方程3. 初始化4. 填表顺序5. 返回值 代码&#xff1a; 2. Z 字形变换题干&#xff1a;算法原理&#xff1a;1. 模拟2. 找规律 代码&#xff1a; 1. 买卖股票的最佳时…

PostGIS学习教程二十:3-D

PostGIS学习教程二十&#xff1a;3-D 注意&#xff1a;本文介绍许多PostGIS2.0及更高版本才支持的功能。 文章目录 PostGIS学习教程二十&#xff1a;3-D一、3-D几何图形二、3-D函数三、N-D索引 一、3-D几何图形 到目前为止&#xff0c;我们一直在处理2-D几何图形&#xff08;…

firewalld防火墙命令行工具

firewall-cmd命令 &#xff08;1&#xff09;启动、停止、查看firewalld服务 在安装CentOS 7系统时&#xff0c;会自动安装firewalld 和图形化工具firewall-config.执行以下命令可 以启动 firewalld 并设置为开机自启动状态。 [rootllcgc ~]# systemctl start firewalld.serv…

【SpringCloud】之入门级及nacos的集成使用

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是君易--鑨&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的博客专栏《SpringCloud开发之入门级及nacos》。&#x1f3…

数据库内核那些事|细说PolarDB优化器查询变换:IN-List变换

导读 数据库的查询优化器是整个系统的"大脑"&#xff0c;一条SQL语句执行是否高效在不同的优化决策下可能会产生几个数量级的性能差异&#xff0c;因此优化器也是数据库系统中最为核心的组件和竞争力之一。阿里云瑶池旗下的云原生数据库PolarDB MySQL版作为领先的云…

36-javascript输出方式,弹框:普通,confirm弹框,prompt弹框,控制台输出:普通,warm,error

1.页面打印 <body><p>你真是一个小机灵鬼</p><script>// 页面打印document.write("打印内容");</script> </body> 2.覆盖文档 <body><p>你真是一个小机灵鬼</p><script>// 覆盖文档window.onload f…

模型容器与AlexNet构建

一、模型容器——Containers nn.Sequential 是 nn.module的容器&#xff0c;用于按顺序包装一组网络层 Sequential 容器 nn.Sequential 是 nn.module的容器&#xff0c;用于按顺序包装一组网络层 • 顺序性&#xff1a;各网络层之间严格按照顺序构建 • 自带forward()&#xf…

HACKTHEBOX通关笔记——Poison(退役)

调试网络连通性 拿到IP我们还是做一下nmap扫描&#xff0c;快速速率扫描结合-A详细扫描&#xff0c;事半功倍 nmap --rate-min 5000 -p- 10.129.58.204 -vnmap -A -p 22,80 10.129.58.204 -v 发现http是一个可以读取文件的页面 这台主机似乎没办法做目录扫描&#xff0c;一扫…

电脑找不到ffmpeg.dll的解决方法有哪些,分享5种可靠的方法

在计算机编程和多媒体处理领域&#xff0c;ffmpeg.dll是一个非常重要的动态链接库文件。它是由FFmpeg项目开发和维护的&#xff0c;FFmpeg是一个开源的音视频处理框架&#xff0c;提供了一套完整的音视频编解码、转码、流化、滤镜等功能。ffmpeg.dll是FFmpeg库的一部分&#xf…

SwiftUI之深入解析Alignment Guides的超实用实战教程

一、Alignment Guide 简介 Alignment guides 是一个强大的布局工具&#xff0c;但通常未被充分利用。在很多情况下&#xff0c;它们可以帮助我们避免更复杂的选项&#xff0c;比如锚点偏好。如下所示&#xff0c;对对齐的更改也可以自动&#xff08;并且容易地&#xff09;动画…

MySQL语法及IDEA使用MySQL大全

在项目中我们时常需要写SQL语句&#xff0c;或简单的使用注解直接开发&#xff0c;或使用XML进行动态SQL之类的相对困难的SQL&#xff0c;并在IDEA中操控我们的SQL&#xff0c;但网上大都图方便或者觉得太简单了&#xff0c;完全没一个涵盖两个方面的讲解。 单表&#xff1a; …

GO语言笔记3-指针

指针的概念 先看一段代码的输出 package main import "fmt" func main(){ var age int 18fmt.Println("age的内存地址值是:",&age)//age的内存地址值是: 0xc000012090// 定义一个指针变量// *int 是一个指针类型&#xff0c;可以理解为指向int类型的…

TEMU 新手小白必看!2024入驻流程/入驻类目/入驻资料等详细流程讲解

2023 TEMU 可谓是赚足眼球&#xff0c;流量持续上涨&#xff0c;2024年相信不少卖家们已经跃跃欲试&#xff0c;但大陆卖家如何入驻TEMU&#xff1f;哪些品类适合入驻&#xff1f;又有哪些入驻要求和资料&#xff1f;别急&#xff0c;今天东哥就一一给大家详细讲解&#xff0c;…

Python操作excel-读取、表格填充颜色区分

1.场景分析 遇到一个需要读取本地excel数据&#xff0c;处理后打入到数据库的场景&#xff0c;使用java比较重&#xff0c;python很好的解决了这类问题 2.重难点 本场景遇到的重难点在于&#xff1a; 需要根据表格内的背景颜色对数据进行筛选 读取非默认Sheet 总是出现Value…

UE5 使用动画模板创建多个动画蓝图

我们制作游戏的时候&#xff0c;角色会根据不同的武器表现出来不同的攻击动画&#xff0c;待机动画以及移动动画。如果我们在UE里面实现这个需求&#xff0c;是通过复制粘贴的方式修改&#xff0c;还是有更好的方式。 这里就需要介绍一下动画模板&#xff0c;我们可以将动画蓝图…

在黑马程序员大学的2023年终总结

起笔 时间真快&#xff0c;转眼又是年末。是时候给2023做个年终总结了&#xff0c;为这一年的学习、生活以及成长画上一个圆满的句号。 这一年相比去年经历了很多事情&#xff0c;接下来我会一一说起 全文大概4000字&#xff0c;可能会占用你15分钟左右的时间 经历 先来给大…

外包干了3个多月,技术退步明显

先说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入广州某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…

【STM32】WDG看门狗

1 WDG简介 WDG&#xff08;Watchdog&#xff09;看门狗 看门狗可以监控程序的运行状态&#xff0c;当程序因为设计漏洞、硬件故障、电磁干扰等原因&#xff0c;出现卡死或跑飞现象时&#xff0c;看门狗能及时复位程序&#xff0c;避免程序陷入长时间的罢工状态&#xff0c;保…

解决不同请求需要的同一实体类参数不同(分组校验validation)

问题概述 新增目录是自动生成id&#xff0c;不需要id参数&#xff1b;更新目录需要id&#xff0c;不能为空 pom.xml中已有spring-boot-starter-validation依赖 <!--validation(完成属性限制&#xff0c;参数校验)--><dependency><groupId>org.springframew…

设计模式的艺术P1基础—2.4-2.11 面向对象设计原则

设计模式的艺术P1基础—2.4-2.11 面向对象设计原则 2.4 面向对象设计原则概述 向对象设计的目标之一在于支持可维护性复用&#xff0c;一方面需要实现设计方案或者源代码的重用&#xff0c;另一方面要确保系统能够易于扩展和修改&#xff0c;具有较好的灵活性。 面向对象设计…
最新文章