Spring Bean循环依赖学习与探究

2023-09-17 15:52:03

本文参考:

画图带你彻底弄懂三级缓存和循环依赖的问题

Spring 三级缓存解决bean循环依赖,为何用三级缓存而非二级_笑矣乎的博客-CSDN博客

Spring为何需要三级缓存解决循环依赖,而不是二级缓存?_石杉的架构笔记的博客-CSDN博客

原理学习

主要的三级缓存工作机理学习参考 画图带你彻底弄懂三级缓存和循环依赖的问题

文章中解决了“是什么”的问题,这里主要梳理下”为什么“的问题,即为什么非得是三级缓存才能解决呢?也就是上面积累下来的问题

七、不用三级缓存,用二级缓存能不能解决循环依赖

遇到这种面试题,你就跟面试官说,如果行的话,Spring的作者为什么不这么写呢?

哈哈,开个玩笑,接下来说说到底为什么不行。

这里我先说一下前面没提到的细节,那就是通过ObjectFactory获取的Bean可能是两种类型,第一种就是实例化阶段创建出来的对象,还是一种就是实例化阶段创建出来的对象的代理对象。至于是不是代理对象,取决于你的配置,如果添加了事务注解又或是自定义aop切面,那就需要代理。这里你不用担心,如果这里获取的是代理对象,那么最后完全创建好的对象也是代理对象,ObjectFactory获取的对象和最终完全创建好的还是同一个,不是同一个肯定会报错,所以上面的理论依然符合,这里只是更加的细节化。

有了这个知识点之后,我们就来谈一下为什么要三级缓存。

第一级缓存,也就是缓存完全创建好的Bean的缓存,这个缓存肯定是需要的,因为单例的Bean只能创建一次,那么肯定需要第一级缓存存储这些对象,如果有需要,直接从第一级缓存返回。那么如果只能有二级缓存的话,就只能舍弃第二级或者第三级缓存。

假设舍弃第三级缓存

舍弃第三级缓存,也就是没有ObjectFactory,那么就需要往第二缓存放入早期的Bean,那么是放没有代理的Bean还是被代理的Bean呢?

1)如果直接往二级缓存添加没有被代理的Bean,那么可能注入给其它对象的Bean跟最后最后完全生成的Bean是不一样的,因为最后生成的是代理对象,这肯定是不允许的;

2)那么如果直接往二级缓存添加一个代理Bean呢?

  • 假设没有循环依赖,提前暴露了代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错,
  • 假设没有循环依赖,使用了ObjectFactory,那么就不会提前暴露了代理对象,到最后生成的对象是什么就是什么,就不会报错,
  • 如果有循环依赖,不论怎样都会提前暴露代理对象,那么如果跟最后创建好的不一样,那么项目启动就会报错

通过上面分析,如果没有循环依赖,使用ObjectFactory,就减少了提前暴露代理对象的可能性,从而减少报错的可能。

假设舍弃第二级缓存

假设舍弃第二级缓存,也就是没有存放早期的Bean的缓存,其实肯定也不行。上面说过,ObjectFactory其实获取的对象可能是代理的对象,那么如果每次都通过ObjectFactory获取代理对象,那么每次都重新创建一个代理对象,这肯定也是不允许的。

从上面分析,知道为什么不能使用二级缓存了吧,第三级缓存就是为了避免过早地创建代理对象,从而避免没有循环依赖过早暴露代理对象产生的问题,而第二级缓存就是防止多次创建代理对象,导致对象不同。

源码溯源

为了从源码角度解决“不用三级缓存,用二级缓存能不能解决循环依赖”的问题,继续去找各种文章去解答:

参考 Spring 三级缓存解决bean循环依赖,为何用三级缓存而非二级_笑矣乎的博客-CSDN博客

DefaultSingletonBeanRegistry.java中有三级缓存,以及如何获取的源码

/**
	 * Return the (raw) singleton object registered under the given name.
	 * <p>Checks already instantiated singletons and also allows for an early
	 * reference to a currently created singleton (resolving a circular reference).
	 * @param beanName the name of the bean to look for
	 * @param allowEarlyReference whether early references should be created or not
	 * @return the registered singleton object, or {@code null} if none found
	 */
	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// Quick check for existing instance without full singleton lock
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				synchronized (this.singletonObjects) {
					// Consistent creation of early reference within full singleton lock
          // 查询一级缓存
					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;
	}

AbstractBeanFactory下面有一个createBean()的中心方法,其核心实现方法doCreateBean(),可以看出来是在类实例化以后,属性填充之前,先检查是否允许提前暴露,如果允许则将其加入三级缓存工厂中
在这里插入图片描述

文章作者认为二级缓存不能解决问题,主要是因为在AOP的场景下,注入到其他bean的,不是AOP代理对象,而是原始对象。但其实这也不完全对,完全可以让AOP代理对象注入到二级缓存中,但是这有悖于AOP的设计原理。AOP就是要等Bean实例化,属性填充,初始化完成之后才去 生成AOP代理对象的,如果使用二级缓存,又要将AOP代理对象注入的话,就需要强行将AOP的代理工作提前到早期暴露实例之前。

但是上面总归还是理论性的知识,在源码层面上,三级缓存究竟做了什么还需要探究,参考

Spring为何需要三级缓存解决循环依赖,而不是二级缓存?_石杉的架构笔记的博客-CSDN博客

三级缓存中key-value加入的是一个beanName 和一个ObjectFactory的工厂类

/**
	 * Add the given singleton factory for building the specified singleton
	 * if necessary.
	 * <p>To be called for eager registration of singletons, e.g. to be able to
	 * resolve circular references.
	 * @param beanName the name of the bean
	 * @param singletonFactory the factory for the singleton object
	 */
	protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(singletonFactory, "Singleton factory must not be null");
		synchronized (this.singletonObjects) {
			if (!this.singletonObjects.containsKey(beanName)) {
				this.singletonFactories.put(beanName, singletonFactory);
				this.earlySingletonObjects.remove(beanName);
				this.registeredSingletons.add(beanName);
			}
		}
	}

在前面获取的时候,也是直接调用ObjectFactory的工厂类的getObject方法

// 若二级缓存不存在,查询三级缓存
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
  singletonObject = singletonFactory.getObject();
  this.earlySingletonObjects.put(beanName, singletonObject);
  this.singletonFactories.remove(beanName);
}

最终调用getEarlyBeanReference方法

/**
	 * Obtain a reference for early access to the specified bean,
	 * typically for the purpose of resolving a circular reference.
	 * @param beanName the name of the bean (for error handling purposes)
	 * @param mbd the merged bean definition for the bean
	 * @param bean the raw bean instance
	 * @return the object to expose as bean reference
	 */
	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;
	}

在这里插入图片描述
从getSingleton()中可以看出,在AOP的情况下,每次singletonFactory.getObject()取出来的都是不同的Aservice代理类,由于Aservice是单例的,这样子显然不行。因此将singletonFactory.getObject()取出来的东西放入二级缓存中,key还是beanName,这样子别的Bean在取的时候直接就可以从二级缓存中拿出来,不必每次调用singletonFactory.getObject()产生一个新的代理对象。

总而言之,在没有AOP的情况下,确实可以通过两级缓存解决循环依赖问题,但是如果加上AOP,两级缓存就不行了。

最后再复习下CGLib动态代理的用法:

package com.jxz.AopTest;

import net.sf.cglib.proxy.Enhancer;

/**
 * @Author jiangxuzhao
 * @Description
 * @Date 2023/9/16
 */
public class CGlibTest {
    public static void main(String[] args) {
        WeakService weakService = new WeakService();

        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(weakService.getClass().getClassLoader());
        enhancer.setSuperclass(weakService.getClass());
        enhancer.setCallback(new JXZMethodInterceptor());

        // 创建代理类
        WeakService proxy = (WeakService)enhancer.create();
        proxy.f("I'm jxz");
    }
}
package com.jxz.AopTest;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @Author jiangxuzhao
 * @Description
 * @Date 2023/9/16
 */
public class JXZMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("增强前...");
        // 这里调用父类方法,不然一直调用本方法死循环了
        Object result = methodProxy.invokeSuper(o, objects);
        System.out.println("增强后...");
        return result;
    }
}

更多推荐

【Leetcode Sheet】Weekly Practice 7

LeetcodeTest1462课程表Ⅳ(9.12)你总共需要上numCourses门课,课程编号依次为0到numCourses-1。你会得到一个数组prerequisite,其中prerequisites[i]=[ai,bi]表示如果你想选bi课程,你必须先选ai课程。有的课会有直接的先修课程,比如如果想上课程1,你

Zookeeper集群 + Kafka集群

Zookeeper集群+Kafka集群Zookeeper概述Zookeeper定义*Zookeeper是一个开源的分布式的,为分布式框架提供协调服务的Apache项目。Zookeeper工作机制*****Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数

PY32F003F18之ADC问题

普然单片机PY32F003F18的内部有一个LDO,其电压固定为1.2V。我在用官方程序测试时,若接上USB转串口的RX导线,向PC发送数据,读内部参考电压比较正确,但是,当接上USB转串口的TX导线时,发现读到内部电压变成了3.3V。见下图:断开USB转串口的TX导线,数据又恢复正常了。用万用表测试该导线电压,为5V

最新中国各地区新能源汽车产量及123个公司公共充电桩数量数据(2015-2023)

数据简介:2010年,《国务院关于加快培育和发展战略性新兴产业的决定》将新能源汽车产业列为战略性新兴产业之一。9月5日,在工信部召开的重点行业稳增长新闻发布会上,工信部装备工业一司司长王卫明表示,今年1—7月,我国新能源汽车产销超过450万辆,同比增长超过40%,其中出口63.6万辆,同比增长1.5倍,成为我国经济的一

go并发(进程、线程、协程)

背景go强大的多线程能力很强大,能并发处理大量的任务。详细案例分类主要介绍go中的进程、线程、协程这三个东西。它们的关系按照内存大小的关系依次是进程>线程>协程(一般一个协程2K)。进程进程基本上是一个正在执行的程序,它是操作系统中最小的资源分配单位。比如电脑上运行的一个软件就是一个进程。go开启进程的方式有三种,本质

MySQL(2) Explain

1、概念使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈2、使用在select语句之前增加explain关键字,MySQL会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL在explain语句后马上执行一条showwarnings语句,会展示mysql对上

Docker

前言:📕作者简介:热爱编程的小七,致力于C、Java、Python等多编程语言,热爱编程和长板的运动少年!📘相关专栏Java基础语法,JavaEE初阶,数据库,数据结构和算法系列等,大家有兴趣的可以看一看。😇😇😇有兴趣的话关注博主一起学习,一起进步吧!一、初识Docker1.1项目部署的问题大型项目组件较多,

Session,cookie,cache,memcache三者的详细讲解

1.SessionSession是一种在服务器端跟踪客户端状态的机制,主要用于在用户与服务器之间建立会话。当用户访问网站时,服务器会为该用户创建一个唯一的会话,并为其分配一个唯一的会话标识符(通常是一个长字符串),这个标识符存储在服务器上。之后,用户和服务器之间的所有交互都通过这个唯一的会话标识符进行标识,以保持状态的

基于时序分析及约束(1)-时序约束是什么?

首先回答标题的问题:时序约束是什么?简单来讲,时序约束就是你要告诉综合工具,你的标准是什么。综合工具应该如何根据你的标准来布线,以满足所以寄存器的时序要求。为什么要做时序约束?这里引用特权同学书中的话:“没有任何设计约束的工程,编译器工作的时候就如脱缰的野马,漫无目的且随意任性;但是,任何的设计过约束或者欠约束,都可能

打造本地紧密链接的开源社区——KCC@长沙开源读书会&openKylin爱好者沙龙圆满举办...

2023年9月9日,由开源社联合openKylin社区举办的KCC@长沙开源读书会&openKylin爱好者沙龙,在长沙圆满举办。这是KCC@长沙首次正式进入公众视野,开展开源交流活动,也是openKylin社区长沙首场线下沙龙。长沙地区及其周边的众多开源爱好者齐聚活动现场,聆听读书分享、参与开源话题讨论,实现1+1>

HAM高可用配置及故障切换

1.什么是MHAMHA(MasterHighAvailability)是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。MHA的出现就是解决MySQL单点的问题。MySQL故障切换过程中,MHA能做到0-30秒内自动完成故障切换操作。MHA能在故障切换的过程中最大程度上保证数据的一致性,以达到真正意义上的高可

热文推荐