首页 > 编程语言 >Spring深度学习:循环依赖及源码解析

Spring深度学习:循环依赖及源码解析

时间:2024-04-09 16:30:55浏览次数:29  
标签:缓存 依赖 对象 Spring beanName 源码 解析 放入

文章目录


Spring深度学习:循环依赖及源码解析

一、序言

大家经常戏称Java工程师为Spring工程师,毫无疑问,这句话体现出Spring框架在Java开发中的重要性和普及度。

本文小豪将带大家深度学习Spring循环依赖相关知识,包括循环依赖的解决方案及Spring处理循环依赖的底层源码,学习过程中不仅仅是了解Spring处理循环依赖的设计,更重要的是学会理解框架的底层逻辑,从而提升我们的思维能力。

文章最后附有流程图,进一步帮我们梳理业务逻辑

二、问题原因

在上一篇【Spring深度学习:Bean生命周期及源码解析】中我们初步了解到,在实例化单例Bean调用getSingleton()方法时,Spring会先分别从一、二、三级缓存中尝试获取单例Bean

那具体这三级缓存分别是什么呢?

/** 一级缓存,存储所有创建好的完整单例Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 三级缓存,存储创建Bean的工厂对象 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** 二级缓存,存储未创建好的半成品单例Bean */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
  • 一级缓存(singletonObjects):存储所有已经创建好的完整单例Bean,即我们常说的单例池缓存
  • 二级缓存(earlySingletonObjects):存储未创建好的半成品单例Bean,即我们常说的提前暴露缓存
  • 三级缓存(singletonFactories):存储创建Bean的工厂对象,即我们常说的工厂对象缓存

Spring三级缓存大致了解了,那Spring为什么需要这三级缓存,具体解决了什么问题呢?没错,解决Bean的循环依赖问题。

循环依赖,就是存在死循环,比如我们存在两个对象AB,如果对象AB分别在属性注入阶段需要注入对方,那就会形成一个死循环,即A需要BB也需要A,无线递归套娃。

具体循环依赖又分为三种类型:
在这里插入图片描述

  1. A自己依赖自己
  2. A依赖BB依赖B
  3. A依赖BB依赖CC依赖A

回到Spring中,通常我们Bean对象进行属性注入常用三种方式:

  • 基于构造方法注入
public class A {

	private B b;

	public A(B b) {
		this.b = b;
	}
}

xml配置文件:

<bean id="bBean" class="com.xiaohao.B"/>

<bean id="aBean" class="com.xiaohao.A">
	<constructor-arg ref="bBean"/>
</bean>
  • 基于set方法注入
public class A {

	private B b;

	public void setB(B b) {
		this.b = b;
	}
}

xml配置文件:

<bean id="bBean" class="com.xiaohao.B"/>

<bean id="aBean" class="com.xiaohao.A">
	<property name="b" ref="bBean"/>
</bean>
  • 基于属性注入
@Component
public class A {

	@Autowired
	private B b;
	
}

上面三种属性注入情况,若产生循环依赖,实际上Spring只能解决后两者,而基于构造方法注入产生的循环依赖,Spring是处理不了的。

这里为什么处理不了基于构造方法注入产生的循环依赖呢?其实Spring解决循环依赖的本质是将实例化和初始化分离,在实例化之后产生缓存,允许其它对象引用。

而构造方法注入,在解析构造方法参数时就会去创建引用对象,而当前类自身还没有完成实例化,如果引用对象又依赖当前对象,则会产生死循环,均无法创建。

三、解决方案

1.普通Bean对象循环依赖解决

通过上面的介绍,我们大概了解到Spring循环依赖产生的场景,那我们应该如何去解决呢?

这里其实很容易想到解决方案,我们可以尝试多加个缓存去解决。

比如我们可以在对象A完成实例化之后,将其放入一个存放不完整Bean的缓存中,具体的执行流程变为:
在这里插入图片描述

  1. 我们在对象A实例化完成之后,属性注入之前,将不完整的对象A放入缓存中
  2. 对象A进行属性注入B时,从缓存中查找B的实例,如果B没有找到,则开始创建B的实例
  3. 同样在对象B实例化完成之后,也将不完整的对象B放入缓存中
  4. 对象B进行属性注入A时,从缓存中查找A的实例,这时由于A的不完整Bean已经放入缓存中,则可以正常获取到,之后对象B完成后续的创建过程,生成完整实例B并放入单例池缓存
  5. B的实例创建完成之后,对象A也正常执行后续流程,完成创建,放入单例池缓存

我们发现,处理循环依赖的情况,额外加一层缓存即可解决循环依赖问题,在Spring中这个放置不完整Bean的缓存也称为提前暴露缓存。

很多同学到这里就会迷茫了,对象B依赖注入A时,注入的是不完整的对象A,也就是对象A并没有执行自身的依赖注入和后续的初始化流程,这样不会有问题吗?

其实这里注入的只是对象A的引用,在JVM堆内存中对象A的地址始终没有发生变化

2.AOP代理场景下循环依赖解决

在上面我们分析到,似乎我们只需要在单例池缓存基础上额外加一层提前暴露缓存即可。

但是这里我们忘了一点,对象A在初始化阶段可能会产生AOP代理对象,最终放入单例池缓存的是A的代理对象,而对象A的属性注入阶段却在初始化阶段之前,这会造成放入提前暴露缓存的是对象A的普通对象,然后对象B注入的是对象A的普通对象,并不是对象A的代理对象,显然存在问题。

很明显,造成这样的问题是由于Bean的生命周期,需要依次执行实例化、属性注入、初始化阶段。

可能大家也很容易想到解决方法,我们是否可以提前产生对象A的AOP代理对象呢?答案是可以的,此时的流程变为:
在这里插入图片描述
大致流程和上面的相同,只不过此时在对象A实例化完成之后,不会直接放入不完整的对象A,而是放入一个A的工厂对象,当检测循环依赖发生之后,根据情况通过工厂对象选择性提前生成对象A的代理对象,赋值给对象B,之后正常完成对象A的初始化,不再重复生成对象A的代理对象。

问题似乎又被我们解决了,只需要工厂对象缓存+单例池缓存即可解决存在AOP代理下的循环依赖问题,但是本文开头,我们介绍了Spring实际上使用了三级缓存,为什么Spring还需要额外多加一层缓存呢?

3.AOP代理场景下多依赖解决

其实这里我们忽略了一直情况,那就是在AOP代理下对象A存在多依赖场景问题,如果对象A同时依赖对象BC,对象BC也依赖对象A,那么在对象B属性注入的时候,生成了A的代理,对象C属性注入的时候,同样也生成了A的代理,此时产生了两个不同的对象A的代理,这显然违背了单例原则。

那我们应该如何解决呢,再加入一层缓存,保存工厂对象缓存生成的对象A的代理对象(提前暴露缓存),此时的业务流程为:
在这里插入图片描述

  1. 在对象A实例化完成之后,属性注入之前,将对象A的工厂对象放入工厂对象缓存中
  2. 对象A进行属性注入B时,依次从提前暴露缓存、工厂对象缓存中查找对象B,均没有找到则开始创建B的实例
  3. 同样在对象B实例化完成之后,也将对象B的工厂对象放入工厂对象缓存中
  4. 对象B进行属性注入A时,依次从提前暴露缓存、工厂对象缓存中查找对象A,这时由于A已经放入工厂对象缓存中,则可以正常获取到,拿到A的工厂对象,生成对象A的代理对象并放入提前暴露缓存,之后对象B完成后续的创建过程,生成实例B并放入单例缓存池
  5. 接着对象A进行属性注入C时,重复流程②-③,进入流程④时,由于对象A的代理对象已经被对象B属性注入时生成并放入提前暴露缓存中,则对象C可以直接通过提前暴露缓存获取到同一个对象A的代理对象,正常完成后续流程,保证了对象A是单例的
  6. 在注入了对象BC之后,对象A也正常执行后续初始化流程,获取提前暴露缓存中已经生成代理对象A,放入单例池缓存中

自此,问题均已经被我们成功解决了。

小豪这里抛出一个疑问,大家可以思考思考,如果我们修改Spring Bean生命周期过程,在实例化Bean之后直接创建Bean的代理,能否只使用二级缓存解决循环依赖问题呢?

接下面我们来详细看一下在Spring底层源码中是如何实现这一过程的。

四、源码分析:

源码分析时,小豪在这里只截取了关键部分源码,便于更高效的梳理整体流程,具体详细源码过程可参考上一篇:【Spring深度学习:Bean生命周期及源码解析】。

同样在这里我们也通过一段流程模拟对象AB的循环依赖。

初始缓存池:
在这里插入图片描述

流程①:实例化对象A后,将对象A的工厂对象放入三级缓存

首先我们进行对象A的创建,进入AbstractAutowireCapableBeanFactory类的doCreateBean("A")方法,在对象A实例化阶段完成之后,判断对象A是否满足循环依赖条件,将对象A放入singletonFactories三级缓存:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){

		// 实例化阶段:进行实例化A对象
		instanceWrapper = createBeanInstance(beanName, mbd, args);
		
		// 循环依赖:单例 && 支持循环引用 && 正在创建
		// 条件成立则将对象A的工厂对象放入singletonFactories三级缓存
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}
    
    	//属性注入阶段:进行对象A的属性填充
		populateBean(beanName, mbd, instanceWrapper);
	}

我们先看一下这段代码,进入addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))方法内部,发现逻辑比较简单,加锁之后将对象A放入singletonFactories三级缓存,同时从earlySingletonObjects二级缓存中移除:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
   synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
         //放入三级缓存
         this.singletonFactories.put(beanName, singletonFactory);
         this.earlySingletonObjects.remove(beanName);
         this.registeredSingletons.add(beanName);
      }
   }
}

getEarlyBeanReference()方法,仔细发现它其实是使用lambda表达式实现的匿名内部类,作为参数二传入addSingletonFactory()方法中,具体它实现什么功能,我们先放一放,继续完成对象A的后续执行。

此时的缓存池:
在这里插入图片描述

流程②:对象A依赖对象B,在缓存中查找对象B

紧接着对象A进入属性注入阶段,发现依赖对象B,则尝试获取对象B,进入doGetBean("B")方法:

protected <T> T doGetBean(
			String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
			throws BeansException {
		// 检查缓存里是否有当前对象B单例(三级缓存)
		Object sharedInstance = getSingleton(beanName);
    	if (sharedInstance != null && args == null) {
        	// 存在则将缓存对象返回出去
			bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    	}

		// 如果不存在则进行实例化当前Bean
		// 使用lambda表达式来实现匿名内部类,传入singletonFactory参数
		sharedInstance = getSingleton(beanName, () -> {
			return createBean(beanName, mbd, args);

		});
		bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
	}

这里首先从三级缓存无法获取到对象B,后续会执行createBean()方法创建对象BcreateBean()方法内部其实又会调用doCreateBean()方法,又会进入对象B流程①阶段。

流程③:实例化对象B后,将对象B的工厂对象放入三级缓存

// 实例化阶段:进行实例化B对象
instanceWrapper = createBeanInstance(beanName, mbd, args);
		
// 将对象B的工厂对象放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

此时的缓存池:
在这里插入图片描述

流程④:对象B依赖对象A,在缓存中查找对象A

此时对象B进入流程②阶段,执行对象B的属性注入,发现依赖对象A,会在缓存中获取到对象A放入的工厂对象:

protected <T> T doGetBean(
			String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
			throws BeansException {
		// 检查缓存里是否有当前对象A单例(三级缓存)
		Object sharedInstance = getSingleton(beanName);
    	if (sharedInstance != null && args == null) {
        	// 存在对象A,直接返回
			bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    	}
	}

流程⑤:通过三级缓存生成不完整的对象A,放入二级缓存

对象B通过getSingleton("A")方法,会依次从一、二、三级缓存中查找对象A,最后会在三级缓存中拿到对象A的工厂对象,调用工厂对象的getObject()方法:

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) {
					// 如果也为空,则从三级缓存中获取对象,此时可以获取到A放入的工厂对象
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						// 调用getEarlyBeanReference()的方法,生成不完整的对象A
                        // 这里根据条件会选择性生成对象A的普通对象或代理对象
						singletonObject = singletonFactory.getObject();
						// 放入二级缓存并在三级缓存中移除
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

在这里由于提前存放的是对象A的工厂对象,调用getObject()方法实际上会触发getEarlyBeanReference()方法,而getEarlyBeanReference()方法具体有什么作用呢?

想必通过上面分析的AOP代理场景下循环依赖解决,大家应该能反应过来。

没错,getEarlyBeanReference()方法其实就是判断对象A是否存在AOP,如果对象A存在AOP,即需要提前创建代理对象A,那么对象B注入的就应该是代理后的对象A,而不是普通对象A

我们进入getEarlyBeanReference()方法源码看一下:

public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		this.earlyProxyReferences.put(cacheKey, bean);
    	// 根据条件判断返回 普通对象A or 代理对象A
		return wrapIfNecessary(bean, beanName, cacheKey);
	}

同时将生成的对象A的普通对象或代理对象放入二级缓存,并在三级缓存中移除:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	if (singletonFactory != null) {
		// 调用getEarlyBeanReference()的方法,生成不完整的对象A
    	// 这里根据条件会选择性生成对象A的普通对象或代理对象
		singletonObject = singletonFactory.getObject();
		// 放入二级缓存并在三级缓存中移除
		this.earlySingletonObjects.put(beanName, singletonObject);
		this.singletonFactories.remove(beanName);
	}
}

此时的缓存池:
在这里插入图片描述

流程⑥:完成对象B的后续创建,放入一级缓存

此时对象B已经成功依赖注入对象A,执行后续初始化流程,完成创建,最后放入一级缓存singletonObjects中:

protected void addSingleton(String beanName, Object singletonObject) {
		synchronized (this.singletonObjects) {
            // 完整的对象B放入一级缓存(单例池)
			this.singletonObjects.put(beanName, singletonObject);
            // 在二、三级缓存中移除
			this.singletonFactories.remove(beanName);
			this.earlySingletonObjects.remove(beanName);
			this.registeredSingletons.add(beanName);
		}
	}

此时的缓存池:
在这里插入图片描述

流程⑦:完成对象A的后续创建,放入一级缓存

对象B创建完成之后,同时对象A也可以成功拿到对象B的完整实例,执行后续初始化流程,完成创建,最后也将对象A放入一级缓存中,此时对象AB均已创建单例完成,解决了循环依赖问题。

最后的缓存池:
在这里插入图片描述

五、流程图

在这里插入图片描述

六、后记

本文从Bean循坏依赖问题产生原因开始,过度到思考其解决方法,同时也带大家阅读底层源码认识Spring如何处理该问题,相信大家看到这里,对Spring循环依赖有了更加清晰的认知。

显然循环依赖不是一个好的代码设计,导致Spring容器大费周章的采用三层缓存去解决它,增加了复杂性。应用在后续的开发中,我们应提前规划好类之间的组织关系,引入相应的接口或抽象类,尽量避免产生循环依赖,降低系统耦合度

小豪在撰写这篇文章时,花费了大量的精力制作流程图,为解释Spring循环依赖问题提供直观的参考。如果大家觉得内容有价值,不妨考虑点点赞,关注关注小豪,后续小豪也会及时更新Spring底层源码其它系列文章哦~

标签:缓存,依赖,对象,Spring,beanName,源码,解析,放入
From: https://blog.csdn.net/mm1274889792/article/details/137556894

相关文章

  • 【全开源】JAVA红娘婚恋相亲交友系统源码支持微信小程序+微信公众号+H5+APP
    JAVA红娘婚恋相亲交友系统源码:跨平台交友新纪元,微信小程序、公众号、H5、APP全覆盖在数字化浪潮汹涌的今天,婚恋相亲已不再是传统的线下模式所能满足。JAVA红娘婚恋相亲交友系统源码,以其卓越的跨平台特性和强大的功能优势,为您打造了一个全新的相亲交友体验。无论是微信小程序、......
  • spring-LocalVariableTableParameterNameDiscoverer
    记录一下后期整理注:此工具类是解析class文件从class文件获取,而不是通过元空间的class对象的method获取 /***在jdk8以前java源码编译后通过反射是无法获得形参名的,在Java8及之后,编译的时候可以通过-parameters为反射生成元信息,可以获取到方法的参数名,但这......
  • idea配置springmvc项目
    传统的web项目(含有webroot文件夹)导入IDEA需要做的一系列配置_ideawebroot-CSDN博客IDEA部署以往的springmvc项目,用外部Tomcat部署---精简版,几步操作完成_springmvc用外置的tomcat-CSDN博客参考这位博主的内容成功配置需要修改的是默认启动文件出现新的问题,js和css文件不好使,......
  • Vue2 + Spring Boot的题库管理和在线考试系统
    一个demo从0到1的搭建~使用mybatisplus快速开发springboot项目(一)--初始化-CSDN博客使用mybatisplus快速开发springboot项目(二)--业务实现_如何用mybatis-plus写业务-CSDN博客使用mybatisplus快速开发springboot项目(三)--JWT拦截器-CSDN博客使用mybatisplus快速开发springboot......
  • Springboot 添加License 以及生成证书和证书验证
    1.先准备生成cer证书及私钥,公钥##(1).生成私匙库#validity:私钥的有效期多少天 365 #alias:私钥别称 privateKey#keystore:指定私钥库文件的名称(生成在当前目录) privateKeys.keystore#storepass:指定私钥库的密码(获取keystore信息所需的密码) public_password#key......
  • java计算机毕业设计元气花艺小程序【附源码+远程部署+程序+mysql】
    本系统(程序+源码)带文档lw万字以上  文末可领取本课题的JAVA源码参考系统程序文件列表系统的选题背景和意义选题背景在现代社会中,随着生活节奏的加快和城市化进程的推进,人们越来越渴望亲近自然、缓解压力。花艺作为一种艺术形式和生活方式,因其独特的审美价值和情感表达功......
  • java计算机毕业设计基于微信小程序的疫情封闭小区自助采购系统【附源码+远程部署+程序
    本系统(程序+源码)带文档lw万字以上  文末可领取本课题的JAVA源码参考系统程序文件列表系统的选题背景和意义选题背景:在新冠疫情的持续影响下,全球范围内的居民生活受到了前所未有的挑战。为了防控疫情的扩散,许多国家和地区不得不采取了封闭管理的措施,限制人员的流动和聚集......
  • java计算机毕业设计基于微信小程序的瑜伽馆约课系统【附源码+远程部署+程序+mysql】
    本系统(程序+源码)带文档lw万字以上  文末可领取本课题的JAVA源码参考系统程序文件列表系统的选题背景和意义标题:基于微信小程序的瑜伽馆约课系统开发在现代都市生活的快节奏中,人们越来越注重身心健康与内在平衡。瑜伽作为一种集身体锻炼、心理放松与精神修养于一体的活动......
  • Springboot计算机毕业设计海滨学院校园墙小程序【附源码】开题+论文+mysql+程序+部署
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容计算机毕业设计海滨学院校园墙小程序研究背景、意义、目的研究背景随着移动互联网技术的快速发展,微信小程序以其便捷性、即用即走的特点,迅速渗透到人们的日......
  • Springboot计算机毕业设计购物商城微信小程序【附源码】开题+论文+mysql+程序+部署
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容计算机毕业设计购物商城微信小程序的研究背景、意义、目的研究背景随着互联网技术的迅猛发展,移动智能终端的普及率不断攀升,微信小程序以其便捷性、轻量级的......