Spring 源码阅读 21:循环依赖和三级缓存

简介: 本文分析了 Spring 容器从缓存中获取单例 Bean 的原理,主要涉及到了 Spring 为解决循环依赖问题,而设计的三级缓存机制。

image.png

基于 Spring Framework v5.2.6.RELEASE

接上篇:Spring 源码阅读 20:获取 Bean 的规范名称

前情提要

上一篇介绍了获取 Bean 实例对象的第一步,根据给定的name获取 Bean 的规范名称。给定的name可能是 Bean 的别名或者 FactoryBean 的逆向引用名称,获取到的规范名称是 Bean 在容器中的唯一标识符。

本文接着介绍 AbstractBeanFactory 中doGetBean方法的下一行关键代码。

ObjectsharedInstance=getSingleton(beanName);

从缓存中获取 Bean 实例对象

直接进入getSingleton方法的源码。

// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)@Override@NullablepublicObjectgetSingleton(StringbeanName) {
returngetSingleton(beanName, true);
}
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)@NullableprotectedObjectgetSingleton(StringbeanName, booleanallowEarlyReference) {
ObjectsingletonObject=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);
            }
         }
      }
   }
returnsingletonObject;
}

这个方法的作用是根据beanName从容器缓存中获取对应的 Bean 实例对象。此处有两个需要注意的地方:

  1. 调用方法传入的参数,是通过transformedBeanName方法获取的规范的 Bean 名称。
  2. 通过调用重载方法,给了allowEarlyReference参数一个默认值true

然后查看方法体中的代码,首先,大概浏览一下方法体的整个流程,可以发现,在容器中有三个集合,Spring 会使用beanName依次从这三个集合中查找 Bean 的实例对象,这三个集合,我们可以将之看作缓存(实际上就是缓存),在 DefaultSingletonBeanRegistry 中可以找到创建它们的代码:

/** Cache of singleton objects: bean name to bean instance. */privatefinalMap<String, Object>singletonObjects=newConcurrentHashMap<>(256);
/** Cache of early singleton objects: bean name to bean instance. */privatefinalMap<String, Object>earlySingletonObjects=newHashMap<>(16);
/** Cache of singleton factories: bean name to ObjectFactory. */privatefinalMap<String, ObjectFactory<?>>singletonFactories=newHashMap<>(16);

这三个 Map 集合的 Key 都是 Bean 的名称,Value 分别是单例 Bean 的实例、早期单例 Bean 的实例、单例 Bean 工厂。

getSingleton方法的开头,会尝试从singletonObjects集合获取 Bean 的实例,获取到就得到了想要的结果,直接返回。

当没有获取到时,如果isSingletonCurrentlyInCreation(beanName)的结果为true,那么尝试从下一个缓存 Map 中获取。isSingletonCurrentlyInCreation方法的源码如下:

// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#isSingletonCurrentlyInCreation/*** Return whether the specified singleton bean is currently in creation* (within the entire factory).* @param beanName the name of the bean*/publicbooleanisSingletonCurrentlyInCreation(StringbeanName) {
returnthis.singletonsCurrentlyInCreation.contains(beanName);
}

这里还是一个集合,根据注释可以知道,这个方法的作用是判断一个单例 Bean 是不是正在被创建,也就是说这个集合中保存了所有正在被创建的 Bean 的名称。

返回到getSingleton方法中,如果从singletonObjects中获取不到,并且这个 Bean 正在被创建,那么就尝试从earlySingletonObjects集合中获取。这里存放的是早期单例 Bean 的实例对象,这些 Bean 实例被创建好,但是还没有进行完初始化,就会被放在这个集合当中,可以提前被获取到了,如果这里获取到了目标 Bean 实例,那么也会作为方法的结果返回。

如果还没有获取到,并且allowEarlyReferencetrue,那么会接着尝试从singletonFactories中获取,这里缓存的是创建早期单例的 Bean 工厂,如果从singletonFactories集合中获取到了这个工厂对象,那么就调用它的getObject方法,将早期 Bean 创建出来,并作为结果返回。

另外,如果早期 Bean 是在这里创建的,那么还需要把创建好的早期 Bean 添加到earlySingletonObjects中,并把工厂从singletonFactories移除掉,因为再次之后,它已经不会被用到了。

你可能会问了:何必呢?直接把 Bean 完整地初始化好放在一个缓存中获取,获取不到就创建新的,不就行了吗,为什么还需要早期实例和工厂实例这种中间状态呢?

这就要讲到循环依赖的问题。

循环依赖

假设接下来我们提到的类型,都是用来创建单例 Bean 的类型,思考这样三种情况:

  1. ClassA 中有一个属性的类型是 ClassA
  2. ClassA 中有一个属性的类型是 ClassB,同时,ClassB 中有一个属性的类型是 ClassA
  3. ClassA 中有一个属性的类型是 ClassB,ClassB 中有一个属性的类型是 ClassC,同时,ClassC 中有一个属性的类型是 ClassA

在之前的 Spring 源码阅读 19:如何 get 到一个 Bean?  一文中,曾经介绍过,完成一个 Bean 的初始化之前,必须要先初始化其各个属性值对应的 Bean。当出现以上三种情况或者类似的形成循环依赖关系的情况,就会出现问题。

以第 3 种情况为例,要完成 ClassA 的初始化,就要先初始化 ClassB,那么就要先初始化 ClassC,初始化 ClassC 又需要完成 ClassA 的初始化。这样就变成了「鸡生蛋蛋生鸡」的问题。

这种情况在实际的开发中,难以完全避免,因此,这是 Spring 必须要解决的问题。

三级缓存

Spring 解决这个问题的办法就是:在一个 Bean 还没有完成初始化的时候,就暴露它的引用。这里就需要用到前面介绍过的三个缓存 Map,也叫三级缓存

以刚刚描述的情况为例,当 ClassA 需要需要一个 ClassB 的单例 Bean 作为属性填充的时候,先创建一个早期的 ClassB 的 Bean,此时,ClassB 刚刚被创建出来,还没有进行属性填充等初始化的流程,就将它放在earlySingletonObjects中,这样,ClassA 的 Bean 实例就可以使用这个 ClassB 的早期实例进行属性填充,ClassB 的早期实例,可以再次之后再进行初始化,这样就不会因为循环依赖关系,导致无法初始化这三个 Bean。

image.png

上图是 Bean 的初始化流程和从各缓存中获取 Bean 实例的流程,详细的过程,会在之后分析到第一次获取一个没有被创建的 Bean 的流程时,再通过代码做深入分析。

局限

不过,Spring 对这个问题的解决还存在局限性。

在 Spring 中,可以通过 setter 方法和构造器将一个 Bean 实例作为属性值注入给另一个 Bean 实例。根据 Spring 通过三级缓存解决循环依赖问题的原理,在一个 Bean 被放入缓存之前,至少需要先通过反射把对象创建出来。但是,如果一个一个属性只能通过构造器注入,那么就无法在注入之前,完成 Bean 对象的创建,也就无法解决循环依赖的问题。

因此,Spring 只能解决通过 setter 注入的循环依赖问题。

总结

本文分析了 Spring 容器从缓存中获取单例 Bean 的原理,主要涉及到了 Spring 为解决循环依赖问题,而设计的三级缓存机制。通过getSingleton方法从缓存中获取 Bean 实例对象,可能获取到,也可能因为 Bean 还完全没有开始创建而获取不到。下一篇,我们接着分析,如果这里获取到了单例 Bean,接下来还需要做哪些处理。

目录
相关文章
|
6天前
|
存储 Java 程序员
Spring 注册BeanPostProcessor 源码阅读
Spring 注册BeanPostProcessor 源码阅读
|
6天前
|
缓存 NoSQL Java
在 Spring Boot 应用中使用 Spring Cache 和 Redis 实现数据查询的缓存功能
在 Spring Boot 应用中使用 Spring Cache 和 Redis 实现数据查询的缓存功能
25 0
|
1月前
|
设计模式 Java 开发者
解密Spring:优雅解决依赖循环的神兵利器
解密Spring:优雅解决依赖循环的神兵利器
188 57
|
2天前
|
缓存 Java 微服务
Springboot微服务整合缓存的时候报循环依赖的错误 两种解决方案
Springboot微服务整合缓存的时候报循环依赖的错误 两种解决方案
9 1
|
2天前
|
缓存 NoSQL Java
后端开发中缓存的作用以及基于Spring框架演示实现缓存
后端开发中缓存的作用以及基于Spring框架演示实现缓存
8 1
|
1月前
|
缓存 Java 开发工具
【spring】如何解决循环依赖
【spring】如何解决循环依赖
141 56
|
8天前
|
缓存 NoSQL Java
在 SSM 架构(Spring + SpringMVC + MyBatis)中,可以通过 Spring 的注解式缓存来实现 Redis 缓存功能
【6月更文挑战第18天】在SSM(Spring+SpringMVC+MyBatis)中集成Redis缓存,涉及以下步骤:添加Spring Boot的`spring-boot-starter-data-redis`依赖;配置Redis连接池(如JedisPoolConfig)和连接工厂;在Service层使用`@Cacheable`注解标记缓存方法,指定缓存名和键生成策略;最后,在主配置类启用缓存注解。通过这些步骤,可以利用Spring的注解实现Redis缓存。
27 2
|
12天前
|
缓存 Java uml
Spring压轴题:当循环依赖遇上Spring AOP
Spring压轴题:当循环依赖遇上Spring AOP
17 1
|
24天前
|
Java Spring 缓存
Spring Bean循环依赖详解
【6月更文挑战第2天】
26 2
|
1月前
|
缓存 NoSQL Java
Spring Cache之本地缓存注解@Cacheable,@CachePut,@CacheEvict使用
SpringCache不支持灵活的缓存时间和集群,适合数据量小的单机服务或对一致性要求不高的场景。`@EnableCaching`启用缓存。`@Cacheable`用于缓存方法返回值,`value`指定缓存名称,`key`定义缓存键,可按SpEL编写,`unless`决定是否不缓存空值。当在类上使用时,类内所有方法都支持缓存。`@CachePut`每次执行方法后都会更新缓存,而`@CacheEvict`用于清除缓存,支持按键清除或全部清除。Spring Cache结合Redis可支持集群环境。
103 6