跳至主要內容

4、Spring 是如何使用三级缓存来解决循环依赖问题?

安图新大约 5 分钟

4、Spring 是如何使用三级缓存来解决循环依赖问题?

1.什么是循环依赖?

假设我们有两个类 A 和 B,类 A 和类 B 的实例互为成员变量即为循环依赖。

 
 

当然也有可能是一个类,或三个类间进行循环依赖。

   

2.什么是 Spring 的循环依赖?

我们这里还是拿上面类 A 和 B 来举例:

 
 

补充知识:Bean 的声明周期如下:

 
 

当 Spring 框架启动后,开始根据注解将类 A 实例化并注入到容器当中,在实例化 A 之后就会尝试获取类 B 的实例来进行依赖注入。

 
 

由于类 B 并没有进行实例化,那么 Spring 框架就会去实例化类 B,同样的也会需要将类 A 的依赖注入到类 B 的实例中:

 
 

最终,无论先实例化哪个类,都会形成死循环。

 
 

简化后如下所示:

 
 

3.三级缓存解决循环依赖

Spring 框架解决循环依赖是通过三级缓存,对应的三级缓存如下所示:

// 单实例对象注册器
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {


	private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
	/** 一级缓存:缓存对象单例,key: Bean的名称,value: Bean的实例 */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
	/** 三级缓存:缓存单例工程: key: Bean的名称,value: 生成对象的工厂 */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
	/** 二级缓存:缓存早期的单例对象: key: Bean的名称,value: Bean的早期实例 */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
}

缓存名称源码名称说明
一级缓存singletonObjects单例池,缓存已经初始化完成的bean对象。
二级缓存earlySingletonObjects缓存早期的bean对象。(例如:只进行了实例化,还没有进行依赖注入)
三级缓存singletonFactories缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的。

我们再看一下上面的循环依赖场景,思考一下:

为什么要使用三级缓存才能解决循环依赖呢?

3.1 假如只使用一级缓存

如果只使用一级缓存,我们可以根据上面的例子看到,类 A 和类 B 都不存在,根本没有初始化完成的对象可以存放到一级缓存中,所以循环依赖没有修复。

3.2 假如使用二级缓存

如果想打破上面循环依赖的死循环,就需要一个中间人来将已经实例化但是没有完成依赖注入的对象给缓存起来,这个中间人就是二级缓存

 
 

然后再配合一级缓存,我们将创建好的单例对象存放到单例池中,同时清空二级缓存中对应的原始对象(半成品实例)。

 
 

看到这里,我们就会有疑问,这不是一级缓存 + 二级缓存已经解决了循环依赖的问题了吗?为什么还需要三级缓存?

3.3 为什么要使用三级缓存

假如类 A 被增强了,那么我们需要注入到 Bean 容器中的就是 A 的代理对象,那么经过上面一整套流程下来,存放到一级缓存中的并不会是代理对象 A,而是对象 A。

为了将对应的代理对象 A 的实例也注入到容器中,这里我们就需要使用三级缓存了。

首先,我们在实例化 A 之后,将 A 中用于创建代理对象 A 的工厂对象 A-ObjectFactory,和 B 中用于创建对象 B 的工厂对象 B-ObjectFactor 放到三级缓存中。

并使用 A 的工厂对象 A-ObjectFactory 作为 A 的实例注入到 A 中。

 
 

然后,我们通过 A 的 ObjectFactory 对象创建 A 的代理对象(半成品/原始对象),然后将 A 的代理对象注入给 B,就可以将 B 创建成功。

 
 

最后,我们将创建好的 B 放入单例池中,然后将 B 注入给 A,这样我们就可以最终将 A 创建成功,然后将创建好的 A 再放入单例池中。

 
 

这样我们就成功使用三级缓存来解决了创建对象时的循环依赖的问题。

4.三级缓存解决循环依赖的局限性

三级缓存只是解决了构造函数之后的循环依赖问题,那么构造函数的循环依赖问题怎么解决呢?

 
 

Spring 给我们提供了一个 @Lazy 注解,也叫懒加载,或延迟加载。被这个注解修饰的对象,只有在使用的时候才会创建实例,那时单例池中的其他对象都已经创建好了,便解决了循环依赖的问题。

整理完毕,完结撒花~ 🌻