热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

Spring循环依赖的解决办法,你真的懂了吗

循坏依赖即循环引用,两个或多个bean相互引用,最终形成一个环。这篇文章主要介绍了Spring循环依赖的解决办法,需要的朋友可以参考下

介绍

先说一下什么是循环依赖,循坏依赖即循环引用,两个或多个bean相互引用,最终形成一个环。Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成

Spring的循环依赖有两种场景

  • 构造器的循环依赖
  • 属性的循环依赖

构造器的循环依赖,可以在构造函数中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入

属性的循环依赖主要是通过3个map来解决的

构造器的循环依赖

@Component
public class ConstructorA {

 private ConstructorB constructorB;

 @Autowired
 public ConstructorA(ConstructorB constructorB) {
 this.cOnstructorB= constructorB;
 }
}
@Component
public class ConstructorB {

 private ConstructorA constructorA;

 @Autowired
 public ConstructorB(ConstructorA constructorA) {
 this.cOnstructorA= constructorA;
 }
}
@Configuration
@ComponentScan("com.javashitang.dependency.constructor")
public class ConstructorConfig {
}
public class ConstructorMain {

 public static void main(String[] args) {
 AnnotationConfigApplicationContext cOntext=
 new AnnotationConfigApplicationContext(ConstructorConfig.class);
 System.out.println(context.getBean(ConstructorA.class));
 System.out.println(context.getBean(ConstructorB.class));
 }
}

运行ConstructorMain的main方法的时候会在第一行就报异常,说明Spring没办法初始化所有的Bean,即上面这种形式的循环依赖Spring无法解决。

我们可以在ConstructorA或者ConstructorB构造函数的参数上加上@Lazy注解就可以解决

@Autowired
public ConstructorB(@Lazy ConstructorA constructorA) {
	this.cOnstructorA= constructorA;
}

因为我们主要关注属性的循环依赖,构造器的循环依赖就不做过多分析了

属性的循环依赖

先演示一下什么是属性的循环依赖

@Component
public class FieldA {

 @Autowired
 private FieldB fieldB;
}
@Component
public class FieldB {

 @Autowired
 private FieldA fieldA;
}
@Configuration
@ComponentScan("com.javashitang.dependency.field")
public class FieldConfig {
}
public class FieldMain {

 public static void main(String[] args) {
 AnnotationConfigApplicationContext cOntext=
 new AnnotationConfigApplicationContext(FieldConfig.class);
 // com.javashitang.dependency.field.FieldA@3aa9e816
 System.out.println(context.getBean(FieldA.class));
 // com.javashitang.dependency.field.FieldB@17d99928
 System.out.println(context.getBean(FieldB.class));
 }
}

Spring容器正常启动,能获取到FieldA和FieldB这2个Bean

属性的循环依赖在面试中还是经常被问到的。总体来说也不复杂,但是涉及到Spring Bean的初始化过程,所以感觉比较复杂,我写个demo演示一下整个过程

Spring的Bean的初始化过程其实比较复杂,为了方便理解Demo,我就把Spring Bean的初始化过程分为2部分

  • bean的实例化过程,即调用构造函数将对象创建出来
  • bean的初始化过程,即填充bean的各种属性

bean初始化过程完毕,则bean就能被正常创建出来了

下面开始写Demo,ObjectFactory接口用来生产Bean,和Spring中定义的接口一样

public interface ObjectFactory {
 T getObject();
}

public class DependencyDemo {

 // 初始化完毕的Bean
 private final Map singletOnObjects=
 new ConcurrentHashMap<>(256);

 // 正在初始化的Bean对应的工厂,此时对象已经被实例化
 private final Map> singletOnFactories=
 new HashMap<>(16);

 // 存放正在初始化的Bean,对象还没有被实例化之前就放进来了
 private final Set singletOnsCurrentlyInCreation=
 Collections.newSetFromMap(new ConcurrentHashMap<>(16));

 public  T getBean(Class beanClass) throws Exception {
 // 类名为Bean的名字
 String beanName = beanClass.getSimpleName();
 // 已经初始化好了,或者正在初始化
 Object initObj = getSingleton(beanName, true);
 if (initObj != null) {
 return (T) initObj;
 }
 // bean正在被初始化
 singletonsCurrentlyInCreation.add(beanName);
 // 实例化bean
 Object object = beanClass.getDeclaredConstructor().newInstance();
 singletonFactories.put(beanName, () -> {
 return object;
 });
 // 开始初始化bean,即填充属性
 Field[] fields = object.getClass().getDeclaredFields();
 for (Field field : fields) {
 field.setAccessible(true);
 // 获取需要注入字段的class
 Class<&#63;> fieldClass = field.getType();
 field.set(object, getBean(fieldClass));
 }
 // 初始化完毕
 singletonObjects.put(beanName, object);
 singletonsCurrentlyInCreation.remove(beanName);
 return (T) object;
 }

 /**
 * allowEarlyReference参数的含义是Spring是否允许循环依赖,默认为true
 * 所以当allowEarlyReference设置为false的时候,当项目存在循环依赖,会启动失败
 */
 public Object getSingleton(String beanName, boolean allowEarlyReference) {
 Object singletOnObject= this.singletonObjects.get(beanName);
 if (singletOnObject== null 
 && isSingletonCurrentlyInCreation(beanName)) {
 synchronized (this.singletonObjects) {
 if (singletOnObject== null && allowEarlyReference) {
  ObjectFactory<&#63;> singletOnFactory=
  this.singletonFactories.get(beanName);
  if (singletonFactory != null) {
  singletOnObject= singletonFactory.getObject();
  }
 }
 }
 }
 return singletonObject;
 }

 /**
 * 判断bean是否正在被初始化
 */
 public boolean isSingletonCurrentlyInCreation(String beanName) {
 return this.singletonsCurrentlyInCreation.contains(beanName);
 }

}

测试一波

public static void main(String[] args) throws Exception {
	DependencyDemo dependencyDemo = new DependencyDemo();
	// 假装扫描出来的对象
	Class[] classes = {A.class, B.class};
	// 假装项目初始化所有bean
	for (Class aClass : classes) {
		dependencyDemo.getBean(aClass);
	}
	// true
	System.out.println(
			dependencyDemo.getBean(B.class).getA() == dependencyDemo.getBean(A.class));
	// true
	System.out.println(
			dependencyDemo.getBean(A.class).getB() == dependencyDemo.getBean(B.class));
}

是不是很简单?我们只用了2个map就搞定了Spring的循环依赖

2个Map就能搞定循环依赖,那为什么Spring要用3个Map呢?

原因其实也很简单,当我们从singletonFactories中根据BeanName获取相应的ObjectFactory,然后调用getObject()这个方法返回对应的Bean。在我们的例子中
ObjectFactory的实现很简单哈,就是将实例化好的对象直接返回,但是在Spring中就没有这么简单了,执行过程比较复杂,为了避免每次拿到ObjectFactory然后调用getObject(),我们直接把ObjectFactory创建的对象缓存起来不就行了,这样就能提高效率了

比如A依赖B和C,B和C又依赖A,如果不做缓存那么初始化B和C都会调用A对应的ObjectFactory的getObject()方法。如果做缓存只需要B或者C调用一次即可。

知道了思路,我们把上面的代码改一波,加个缓存。

public class DependencyDemo {

	// 初始化完毕的Bean
	private final Map singletOnObjects=
			new ConcurrentHashMap<>(256);

	// 正在初始化的Bean对应的工厂,此时对象已经被实例化
	private final Map> singletOnFactories=
			new HashMap<>(16);

	// 缓存Bean对应的工厂生产好的Bean
	private final Map earlySingletOnObjects=
			new HashMap<>(16);

	// 存放正在初始化的Bean,对象还没有被实例化之前就放进来了
	private final Set singletOnsCurrentlyInCreation=
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));

	public  T getBean(Class beanClass) throws Exception {
		// 类名为Bean的名字
		String beanName = beanClass.getSimpleName();
		// 已经初始化好了,或者正在初始化
		Object initObj = getSingleton(beanName, true);
		if (initObj != null) {
			return (T) initObj;
		}
		// bean正在被初始化
		singletonsCurrentlyInCreation.add(beanName);
		// 实例化bean
		Object object = beanClass.getDeclaredConstructor().newInstance();
		singletonFactories.put(beanName, () -> {
			return object;
		});
		// 开始初始化bean,即填充属性
		Field[] fields = object.getClass().getDeclaredFields();
		for (Field field : fields) {
			field.setAccessible(true);
			// 获取需要注入字段的class
			Class<&#63;> fieldClass = field.getType();
			field.set(object, getBean(fieldClass));
		}
		singletonObjects.put(beanName, object);
		singletonsCurrentlyInCreation.remove(beanName);
		earlySingletonObjects.remove(beanName);
		return (T) object;
	}

	/**
	 * allowEarlyReference参数的含义是Spring是否允许循环依赖,默认为true
	 */
	public 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<&#63;> singletOnFactory=
							this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletOnObject= singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}
}

我们写的getSingleton的实现和org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)的实现一模一样,这个方法几乎所有分析Spring循环依赖的文章都会提到,这次你明白工作原理是什么了把

总结一波

  • 拿bean的时候先从singletonObjects(一级缓存)中获取
  • 如果获取不到,并且对象正在创建中,就从earlySingletonObjects(二级缓存)中获取
  • 如果还是获取不到就从singletonFactories(三级缓存)中获取,然后将获取到的对象放到earlySingletonObjects(二级缓存)中,并且将bean对应的singletonFactories(三级缓存)清除
  • bean初始化完毕,放到singletonObjects(一级缓存)中,将bean对应的earlySingletonObjects(二级缓存)清除

参考博客

[1]https://mp.weixin.qq.com/s/gBr3UfC1HRcw4U-ZMmtRaQ
[2]https://mp.weixin.qq.com/s/5mwkgJB7GyLdKDgzijyvXw
比较详细
[1]https://zhuanlan.zhihu.com/p/84267654
[2]https://juejin.im/post/5c98a7b4f265da60ee12e9b2

到此这篇关于Spring循环依赖的解决办法,你真的懂了吗的文章就介绍到这了,更多相关Spring循环依赖内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了使用postman进行接口测试的方法,以测试用户管理模块为例。首先需要下载并安装postman,然后创建基本的请求并填写用户名密码进行登录测试。接下来可以进行用户查询和新增的测试。在新增时,可以进行异常测试,包括用户名超长和输入特殊字符的情况。通过测试发现后台没有对参数长度和特殊字符进行检查和过滤。 ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 在Kubernetes上部署JupyterHub的步骤和实验依赖
    本文介绍了在Kubernetes上部署JupyterHub的步骤和实验所需的依赖,包括安装Docker和K8s,使用kubeadm进行安装,以及更新下载的镜像等。 ... [详细]
  • 打开文件管理器_【教程】模组管理器3.1食用指南
    文编:byakko最近有部分小伙伴反应还不会使用unity模组管理器,现在我就给大家讲一下unity模组管理器——从下载到使用。完整视频版以下是无WiF ... [详细]
  • 本文详细介绍了C语言中的格式化字符串类型,包括浮点数、十六进制数字、p-计数法、十进制整数、小数形式、实数、有符号整数和输出字符串等。对于每种类型,都给出了详细的解释和示例。通过本文的学习,读者将对C语言中的格式化字符串类型有更深入的理解。 ... [详细]
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
author-avatar
一切皆空2502861573
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有