Spring循环依赖场景分析

发布时间 2023-12-02 00:09:11作者: 街头卖艺的肖邦

循环依赖概述

在Spring中,当实例化完成之后,要开始进行初始化赋值操作,但是赋值的时候,值的类型有可能是引入类型,需要从Spring容器中获取具体的某个对象来完成赋值操作,而此时,需要引入的对象可能被创建了,也可能没被创建,如果被创建了,那么直接获取即可,如果没有被创建,在整个初始化过程中就会涉及到对象的创建过程,而当前对象的创建过程中又会有其他的依赖,其他的依赖中可能包含当前的对象,而此时的对象还没创建完成,则会回到当前对象创建的开始,此时产生了循环依赖的问题;

Spring是支持基于单例Bean的Setter方法的循环依赖,但是有一种特殊情况需要注意;

 

循环依赖场景测试

单例Bean的Setter循环依赖

场景一

查看代码
@Component
public class BeanA {

	@Autowired
	private BeanB beanB;

	public BeanB getBeanB() {
		return beanB;
	}
}

 

查看代码
@Component
public class BeanB {
	@Autowired
	private BeanA beanA;
    
	public BeanA getBeanA() {
		return beanA;
	}
}

这种场景下操作对象获取依赖对象是可以正常运行的;

 

场景二

查看代码
@Component
public class BeanA {

	@Autowired
	private BeanB beanB;

	public void setBeanB(BeanB beanB) {
		this.beanB = beanB;
	}

	@Override
	public String toString() {
		return "BeanA{" +
				"beanB=" + beanB +
				'}';
	}
}

 

查看代码
@Component
public class BeanB {
	@Autowired
	private BeanA beanA;

	public void setBeanA(BeanA beanA) {
		this.beanA = beanA;
	}

	@Override
	public String toString() {
		return "BeanB{" +
				"beanA=" + beanA +
				'}';
	}
}

这种场景操作对象获取依赖对象时会报错,如下图;

这个StackOverflowError报错不是Spring报的,它是由于对象间相互调用toString方法,造成递归,最终栈溢出的;

 

多例的Setter循环依赖

查看代码
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Component
public class PrototypeCircularBeanA {
	@Autowired
	private PrototypeCircularBeanB prototypeCircularBeanB;

	public void setPrototypeCircularBeanB(PrototypeCircularBeanB prototypeCircularBeanB) {
		this.prototypeCircularBeanB = prototypeCircularBeanB;
	}
}

 

查看代码
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Component
public class PrototypeCircularBeanB {

	@Autowired
	private PrototypeCircularBeanA prototypeCircularBeanA;

	public void setPrototypeCircularBeanA(PrototypeCircularBeanA prototypeCircularBeanA) {
		this.prototypeCircularBeanA = prototypeCircularBeanA;
	}
}

这种场景操作对象获取依赖对象时会报错,如下图;

在AbstractBeanFactory#doGetBean这个方法里有以下几个判断,处理了多例Bean依赖;

Spring对多例对象不能解决循环依赖是这里控制的,因为IOC容器对多例对象不能进行缓存,所以无法暴露早期对象,每次调用都会创建新的对象;

 

代理对象的循环依赖

查看代码
@Component("ProxyCircularBeanA")
public class ProxyCircularBeanA {

	@Autowired
	private ProxyCircularBeanB proxyCircularBeanB;

	@Async
    public ProxyCircularBeanB getProxyCircularBeanB() {
        return proxyCircularBeanB;
    }
}

 

@Component("ProxyCircularBeanB")
public class ProxyCircularBeanB {
	@Autowired
    private ProxyCircularBeanA proxyCircularBeanA;

    public ProxyCircularBeanA getProxyCircularBeanA() {
        return proxyCircularBeanA;
    }
}

这种场景操作对象获取依赖对象时会报错,如下图;

这个在下面分析;

 

构造方法的循环依赖

查看代码
@Component
public class ConstructCircularBeanA {

	private ConstructCircularBeanB constructCircularBeanB;

	public ConstructCircularBeanA(ConstructCircularBeanB constructCircularBeanB) {
		this.constructCircularBeanB = constructCircularBeanB;
	}
}

 

查看代码
@Component
public class ConstructCircularBeanB {
	private ConstructCircularBeanA constructCircularBeanA;

	public ConstructCircularBeanB(ConstructCircularBeanA constructCircularBeanA) {
		this.constructCircularBeanA = constructCircularBeanA;
	}
}

这种场景操作对象获取依赖对象时会报错,如下图;

抛出异常是在DefaultSingletonBeanRegistry#beforeSingletonCreation方法,如下;

其中,singletonsCurrentlyInCreation是一个Set集合,在在创建单例Bean之前,Spring会将正在创建的Bean的名称添加到singletonsCurrentlyInCreation集合中

beforeSingletonCreation这个方法是在创建单例Bean之前,它会将Bean的名称添加到singletonsCurrentlyInCreation集合中,添加失败说明该集合已经存在该Bean的名称,发生了循环依赖,

 

@DependsOn的循环依赖

查看代码
@Component("DependsOnCircularBeanA")
@DependsOn("DependsOnCircularBeanB")
public class DependsOnCircularBeanA {
	private DependsOnCircularBeanB dependsOnCircularBeanB;

	public DependsOnCircularBeanB getDependsOnCircularBeanB() {
		return dependsOnCircularBeanB;
	}
}

 

查看代码
 @Component("DependsOnCircularBeanB")
@DependsOn("DependsOnCircularBeanA")
public class DependsOnCircularBeanB {
	@Autowired
	private DependsOnCircularBeanA dependsOnCircularBeanA;

	public DependsOnCircularBeanA getDependsOnCircularBeanA() {
		return dependsOnCircularBeanA;
	}
}

这种场景操作对象获取依赖对象时会报错,如下图;

在AbstractBeanFactory#doGetBean这个方法里有以下几个判断,处理了对象间通过@DependsOn依赖;
Spring对dependsOn的依赖是在这里处理,如果对象间有dependsOn相互的依赖,则会抛出BeanCreationException的异常;

 

循环依赖产生分析

Spring默认是仅支持单例Bean的Setter方法的注入,而在上面单例Bean循环依赖测试的场景二在执行时抛出StackOverflowError,这个错误不是Spring抛出的,它是因为对象之间相互调用对方的toString方法,造成的递归;
 

Spring中的三级缓存

在Spring的DefaultSingletonBeanRegistry类有三个Map,它们专门用于解决对象间循环依赖的问题;
  • 一级缓存

singletonObjects,存放完全实例化属性赋值完成的Bean,直接可以使用;

  • 二级缓存

earlySingletonObjects,存放早期Bean的引用,尚未属性装配的Bean;

  • 三级缓存

singletonFactories,三级缓存,存放实例化完成的Bean工厂,但此时实例化完的Bean依旧属于早期Bean对象;

 

对象创建的getBean操作

在Spring中,对象创建最终都是会调用AbstractBeanFactory#getBean方法,而该方法只是一个空壳方法,没有实现任何逻辑,Spring一般真正的处理逻辑是doXXX开头的,这里对应的是doGetBean方法;
而AbstractBeanFactory#getBean方法一开始是会先调用DefaultSingletonBeanRegistry#getSingleton(String, boolean)获取对象,入参分别传入beanName和true;
  • DefaultSingletonBeanRegistry#getSingleton(String, boolean)
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock
    /**
		 * 第一步:我们去一级缓存(单例缓存池中去获取对象,一般情况从该map中获取的对象是直接可以使用的)
		 * IOC容器初始化加载单实例bean的时候第一次进来的时候 该map中一般返回空
		 */
    Object singletonObject = this.singletonObjects.get(beanName);
    /**
		 * 若在第一级缓存中没有获取到对象,并且singletonsCurrentlyInCreation这个list包含该beanName
		 * IOC容器初始化加载单实例bean的时候第一次进来的时候 该list中一般返回空,但是循环依赖的时候可以满足该条件
		 */
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        /**
			 * 去二级缓存中获取对象(二级缓存中的对象是一个早期对象)
			 * 何为早期对象:就是bean刚刚调用了构造方法,还来不及给bean的属性进行赋值的对象
			 * 就是早期对象
			 */
        singletonObject = this.earlySingletonObjects.get(beanName);
        /**
			 * 二级缓存中也没有获取到对象,allowEarlyReference为true(参数是有上一个方法传递进来的true)
			 */
        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对象 这个对接就是用来解决循环依赖的关键所在
							 * 在ioc后期的过程中,当bean调用了构造方法的时候,把早期对象包裹成一个ObjectFactory
							 * 暴露到三级缓存中
							 */
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            /**
								 * 在这里通过暴露的ObjectFactory 包装对象中,通过调用他的getObject()来获取我们的早期对象
								 * 在这个环节中会调用到 getEarlyBeanReference()来进行后置处理
								 */
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}
上面的singletonFactory是ObjectFactory,它会通过getObject方法获取对象,如下图;
getObject方法会触发如下调用:
AbstractAutowireCapableBeanFactory#createBean(String, RootBeanDefinition, Object[])
->
  AbstractAutowireCapableBeanFactory#doCreateBean
在AbstractAutowireCapableBeanFactory#doCreateBean方法中会判断是否将三级缓存提前暴露;
通过getEarlyBeanReference可获取代理对象,在BeanA初始化完成前将创建的代理对象实例的ObjectFactory加入三级缓存;
 
之后会判断早期对象和二级缓存是否相等,其中bean为早期对象,exposedObject是从二级缓存获取到实例;
至此,循环依赖处理结束;
在上面@Async注解的代理对象循环依赖的场景中,Spring生成的一个早期对象的bean,另一个是创建代理对象的exposedObject,此时两个对象判断是不一致的;
要解决该问题,要么破坏对象间的循环依赖,或更改类的加载顺序,而更改类的加载顺序有如下方式:
  • 使用@Lazy注解,延迟加载
  • 使用@DependsOn注解,指定加载的先后关系
  • 修改文件名称,改变循环依赖类的加载顺序(Spring是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载
 

循环依赖的处理流程

注:
  • 在属性注入的时候,调用该对象生成的时候检测是否需要被代理,如果需要,则直接创建代理对象;
  • 在整个过程中,没有其他的对象有当前对象的依赖,那么在生成最终的完整对象之前(Bean初始化initializeBean方法)生成代理对象即可(BeanPostProcessor的after方法);

 

再谈Spring中的三级缓存

首先抛开Spring的实现来做一次循环依赖的设计推演;

在没有缓存的情况下发生循环依赖

从上图可以直接观察到,当没有缓存时,当发生循环依赖时发生了死循环,最终造成StackOverflow或者OOM;

 

增加一级缓存

从上图可以直观的看出,循环依赖的问题已经得到了解决,但同时又带来了一个新问题,这个缓存中的Bean对象可能有已经创建完成的、正在创建中还没有注入依赖的,它们掺杂在一块,需要编写更多的判断逻辑去区分缓存中的对象是否是已经创建完成的,而不是正在创建的,显然一级缓存缺乏安全性与扩展性;

首先从Spring循环依赖的处理流程可以看出如果对象间发生了循环依赖,那必定是会对二级缓存获取对象,在Spring实现中,二级缓存中主要存储是未对属性赋值的Bean,区分开已经完成创建的Bean和未对属性赋值的Bean,这样能防止在多线程环境下,读取到还没创建的Bean;

 

增加二级缓存

新增了二级缓存,用于存放未对属性赋值的Bean,这可以区分Bean的完整性,但是如何区分普通对象和代理对象?

Spring中代理对象的创建有两处位置,如下:

  • 在属性注入的时候,调用该对象生成的时候检测是否需要被代理,如果需要,则直接创建代理对象
  • 在整个过程中,没有其他的对象有当前对象的依赖,那么在生成最终的完整对象之前(Bean初始化initializeBean方法)生成代理对象即可(BeanPostProcessor的after方法);

从上图可以看出最终一级缓存中存储的BeanA是代理对象,而BeanB注入的是一个早期的BeanA,这里出现问题;

假如在二级缓存获取到时创建代理对象,这样能解决?

从上图可以看出BeanA创建了两次代理对象,一次是在获取到二级缓存后生成的代理对象,另一次是在BeanA初始化完后生成的代理对象,这两个对象是完全不一样的,也就不是单例了;

下面看下Spring循环依赖处理实现;

 

三级缓存

BeanA在初始化时,Spring会先查二级缓存中有没有BeanA的代理,如果有查到则说明发生了循环依赖,之后判断早期创建的BeanA与初始化后返回的对象是否相等,如果相等则将BeanA代理对象放入一级缓存,删除BeanA的二级缓存,完成对象创建,如果不相等则抛出循环依赖的异常,这个对应上面的代理对象循环依赖的场景;

在Spring中,三级缓存存储的是ObjectFactory,每次需要从三级缓存获取对象实例就会调用其实现的getObject方法,从而获取到对应的二级缓存;假如没有二级缓存earlySingletonObjects,只有一级缓存和三级缓存,此时有三个对象,依赖关系如下:

代理对象A -> B
B -> 代理对象A 
B -> C
C -> 代理对象A

A被B和C依赖,共依赖了两次,A对应的ObjectFactory会调用两次getObject方法,对于生成代理对象可能会有不同,在后置处理器调用和属性赋值会有生成代理对象的逻辑,这样会生成不同的代理对象,B和C依赖的属性A的代理对象不一致;

如果单纯为了解决循环依赖问题,那么使用二级缓存足够解决问题,使用三级而非二级缓存并非出于IOC的考虑,而是出于AOP的考虑,如果使用二级缓存,在AOP情形下,注入到其他Bean的,不是最终的代理对象,而是早期对象,三级缓存能避免了代理对象的重复创建,代理对象重复创建会带来生成Bean代理对象和被依赖对象属性赋值时生成的代理对象不一致的问题;