介绍

先说一下什么是循环依赖,Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成

Spring的循环依赖有两种场景

  1. 构造器的循环依赖
  2. 属性的循环依赖

构造器的循环依赖,可以在构造函数中使用@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部分

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

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

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

public interface ObjectFactory<T> {
T getObject();
}
public class DependencyDemo {

	// 初始化完毕的Bean
private final Map<String, Object> singletonObjects =
new ConcurrentHashMap<>(256); // 正在初始化的Bean对应的工厂,此时对象已经被实例化
private final Map<String, ObjectFactory<?>> singletonFactories =
new HashMap<>(16); // 存放正在初始化的Bean,对象还没有被实例化之前就放进来了
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>(16)); public <T> T getBean(Class<T> 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<?> 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<?> 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<String, Object> singletonObjects =
new ConcurrentHashMap<>(256); // 正在初始化的Bean对应的工厂,此时对象已经被实例化
private final Map<String, ObjectFactory<?>> singletonFactories =
new HashMap<>(16); // 缓存Bean对应的工厂生产好的Bean
private final Map<String, Object> earlySingletonObjects =
new HashMap<>(16); // 存放正在初始化的Bean,对象还没有被实例化之前就放进来了
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>(16)); public <T> T getBean(Class<T> 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<?> 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<?> 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循环依赖的文章都会提到,这次你明白工作原理是什么了把

总结一波

  1. 拿bean的时候先从singletonObjects(一级缓存)中获取
  2. 如果获取不到,并且对象正在创建中,就从earlySingletonObjects(二级缓存)中获取
  3. 如果还是获取不到就从singletonFactories(三级缓存)中获取,然后将获取到的对象放到earlySingletonObjects(二级缓存)中,并且将bean对应的singletonFactories(三级缓存)清除
  4. 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如何解决循环依赖?的更多相关文章

  1. Spring 如何解决循环依赖问题?

    在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的. 这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能 ...

  2. Spring如何解决循环依赖问题

    目录 1. 什么是循环依赖? 2. 怎么检测是否存在循环依赖 3. Spring怎么解决循环依赖 本文主要是分析Spring bean的循环依赖,以及Spring的解决方式. 通过这种解决方式,我们可 ...

  3. Spring 如何解决循环依赖的问题

    Spring 如何解决循环依赖的问题 https://blog.csdn.net/qq_36381855/article/details/79752689 Spring IOC 容器源码分析 - 循环 ...

  4. 一张图彻底理解Spring如何解决循环依赖!!

    写在前面 最近,在看Spring源码,看到Spring解决循环依赖问题的源码时,不得不说,源码写的太烂了.像Spring这种顶级的项目源码,竟然存在着这种xxx的代码.看了几次都有点头大,相信很多小伙 ...

  5. Spring如何解决循环依赖

    一.什么是循环依赖 多个bean之间相互依赖,形成了一个闭环. 比如:A依赖于B.B依赖于c.c依赖于A 通常来说,如果问spring容器内部如何解决循环依赖, 一定是指默认的单例Bean中,属性互相 ...

  6. Spring中解决循环依赖报错的问题

    什么是循环依赖 当一个ClassA依赖于ClassB,然后ClassB又反过来依赖ClassA,这就形成了一个循环依赖: ClassA -> ClassB -> ClassA 原创声明 本 ...

  7. Spring如何解决循环依赖,你真的懂了?

    导读 前几天发表的文章SpringBoot多数据源动态切换和SpringBoot整合多数据源的巨坑中,提到了一个坑就是动态数据源添加@Primary接口就会造成循环依赖异常,如下图: 这个就是典型的构 ...

  8. 彻底理解Spring如何解决循环依赖

    Spring bean生命周期 可以简化为以下5步. 1.构建BeanDefinition 2.实例化 Instantiation 3.属性赋值 Populate 4.初始化 Initializati ...

  9. 【Spring】 Spring如何解决循环依赖的问题?

    https://mp.weixin.qq.com/s/FtbzTMxHgzL0G1R2pSlh-A 通常来说,如果问Spring内部如何解决循环依赖,一定是单默认的单例Bean中,属性互相引用的场景. ...

随机推荐

  1. Rocket - util - LanePositionedQueue

    https://mp.weixin.qq.com/s/yO_9Ec3S5-AosRVLpsBgOg   简单介绍基于通道位置的队列(LanePositionedQueue)的实现.   ​​   1. ...

  2. LeetCode 73,为什么第一反应想到的解法很有可能是个坑?

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode第42篇文章,我们来看看LeetCode第73题矩阵置零,set matrix zeroes. 这题的难度是Mediu ...

  3. 【Linux】CentOS 7安装与使用,安装jdk1.8,安装mysql

    虚拟机软件VMware Workstation Pro 安装CentOS7,准备好镜像文件! 一定要勾选"启动时连接" 语言选英语 时区点击上海 确认硬盘,就是点击黄色警告的那个, ...

  4. Java实现 蓝桥杯 算法提高 字符串匹配

    试题 算法提高 字符串匹配 问题描述 给出一个字符串和多行文字,在这些文字中找到字符串出现的那些行.你的程序还需支持大小写敏感选项:当选项打开时,表示同一个字母的大写和小写看作不同的字符:当选项关闭时 ...

  5. Java实现 蓝桥杯 历届真题 稍大的串

    串可以按照字典序进行比较.例如: abcd 小于 abdc 如果给定一个串,打乱组成它的字母,重新排列,可以得到许多不同的串,在这些不同的串中,有一个串刚好给定的串稍微大一些.科学地说:它是大于已知串 ...

  6. Java实现字符串的包含

    1 问题描述 给定一长字符串A和一短字符串B.请问,如何最快地判断出短字符串B中的所有字符是否都在长字符串A中?请编写一个判断函数实现此功能. 为简单起见,假设输入的字符串只包含小写英文字母.下面举几 ...

  7. Java实现 洛谷 P1089 津津的储蓄计划

    import java.util.*; public class Main { public static void main(String[] args) { Scanner sc=new Scan ...

  8. Java实现 蓝桥杯 历届试题 约数倍数选卡片

    问题描述 闲暇时,福尔摩斯和华生玩一个游戏: 在N张卡片上写有N个整数.两人轮流拿走一张卡片.要求下一个人拿的数字一定是前一个人拿的数字的约数或倍数.例如,某次福尔摩斯拿走的卡片上写着数字" ...

  9. tcpdump 基于mac地址抓取数据包

    1.刚刚接触tcpdump时,常用tcpdump -i eth1 host 192.168.1.1 这个命令基于ip地址抓取数据包信息. tcpdump -i eth1(接口名称) host 192. ...

  10. Android事件传递机制总结

    Android中控件的分类 Activity dispatchTouchEvent(MotionEvent e) onTouchEvent(MotionEvent e) ViewGroup(View) ...