Spring如何解决循环依赖问题(面试题)

Java技术 潘老师 2年前 (2022-05-05) 1039 ℃ (0) 扫码查看

对于循环依赖问题,Spring框架早就考虑到了,那么Spring到底是如何解决循环依赖问题的?这个问题不仅在面试题中也会经常被问到,我们在日常开发中也会经常遇到该问题,当循环依赖问题出现时,有部分spring支持的,可以自行帮我们解决了,但也有部分不支持,我们要能知道原因,并能找到解决方案。

一、针对面试题

如果值针对面试题问:”Spring是如何解决循环依赖问题的?”,我们可以回答如下:

要想彻底搞明白Spring是如何通过三级缓存来解决循环依赖问题,就需要了解很多相关知识,包括一些源码知识,下面我们一起来看看。

二、什么是循环依赖

循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。

三、循环依赖的类型

主要有3种类型:
第一种情况:自己依赖自己的直接依赖
自依赖
第二种情况:两个对象之间的直接依赖
两个对象直接依赖
第三种情况:多个对象之间的间接依赖
多个对象间接依赖
前面两种情况的直接循环依赖比较直观,非常好识别,但是第三种间接循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。

四、Spring中循环依赖的几种场景及Spring支持情况

spring中出现循环依赖主要有以下场景,其中部分场景Spring是支持的,可以自行帮我们解决了。
Spring中循环依赖的几种场景及Spring支持情况
下面就通过源码探究这些场景 Spring 支持或不支持的原因。要搞懂这些,我们需要重点搞懂以下几个问题:

  • 搞清楚Spring三级缓存的作用?
  • 搞清楚第三级缓存中ObjectFactory的作用?
  • 搞清楚为什么需要第二级缓存?
  • 搞清楚什么时候使用三级缓存(添加和查询操作)?
  • 搞清楚什么时候使用二级缓存(添加和查询操作)?
  • 当目标对象产生代理对象时,Spring容器中(第一级缓存)到底存储的是谁?

我们带着这些问题一起来看看。

第 ① 种场景——单例 Bean 的 setter 注入

这种使用方式也是最常用的方式之一,假设有两个 Service 分别为 OrderService(订单相关业务逻辑)和 TradeService(交易相关业务逻辑),代码如下:

@Service
public class OrderService {
  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }
}
@Service
public class TradeService {
  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }
}

这种循环依赖场景,程序是可以正常运行的,从代码上看确实是有循环依赖了,也就是说 Spring 是支持这种循环依赖场景的,这里我们察觉不到循环依赖的原因是 Spring 已经默默地解决了。

假设没有做任何处理,按照正常的创建逻辑来执行的话,流程是这样的:容器先创建 OrderService,发现依赖于 TradeService,再创建 OrderService,又发现依赖于 TradeService … ,发生无限死循环,最后发生栈溢出错误,程序停止。为了支持这种常见的循环依赖场景,Spring 将创建对象分为如下几个步骤:

实例化一个新对象(在堆中),但此时尚未给对象属性赋值
给对象赋值
调用 BeanPostProcessor 的一些实现类的方法,在这个阶段,Bean 已经创建并赋值属性完成。这时候容器中所有实现 BeanPostProcessor 接口的类都会被调用(e.g. AOP)
初始化(如果实现了 InitializingBean,就会调用这个类的方法来完成类的初始化)
返回创建出来的实例
为此,Spring 引入了三级缓存来处理这个问题(三级缓存定义在 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 中),第一级缓存 singletonObjects 用于存放完全初始化好的 Bean,从该缓存中取出的 Bean 可以直接使用,第二级缓存 earlySingletonObjects 用于存放提前暴露的单例对象的缓存,存放原始的 Bean 对象(属性尚未赋值),用于解决循环依赖,第三级缓存 singletonFactories 用于存放单例对象工厂的缓存,存放 Bean 工厂对象,用于解决循环依赖。上述实例使用三级缓存的处理流程如下所示:
使用三级缓存的处理流程
如果你看过三级缓存的定义源码的话,可能也有这样的疑问:为什么第三级的缓存的要定义成 Map>,不能直接缓存对象吗?这里不能直接保存对象实例,因为这样就无法对其做增强处理了。详情可见类 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean 如下:doCreateBean 方法部分源码

第 ② 种场景——多例 Bean 的 setter 注入

这种方式平常使用得相对较少,还是使用前文的两个 Service 作为示例,唯一不同的地方是现在都声明为多例了,示例代码如下:

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderService {
  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }
}
/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TradeService {
  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }
}

如果你在 Spring 中运行以上代码,是可以正常启动成功的,原因是在类 org.springframework.beans.factory.support.DefaultListableBeanFactory 的 preInstantiateSingletons() 方法预实例化处理时,过滤掉了多例类型的 Bean,方法部分代码如下:

但是如果此时有其它单例类型的 Bean 依赖到这些多例类型的 Bean 的时候,就会报如下所示的循环依赖错误了。循环依赖错误

第 ③ 种场景——代理对象的 setter 注入

这种场景也会经常碰到,有时候为了实现异步调用会在 XXXXService 类的方法上添加 @Async 注解,让方法对外部变成异步调用(前提要是要在启用类上添加启用注解哦 @EnableAsync),示例代码如下:

@EnableAsync
@SpringBootApplication
public class BlogMghioCodeApplication {

  public static void main(String[] args) {
    SpringApplication.run(BlogMghioCodeApplication.class, args);
  }
}
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderService {
  @Autowired
  private TradeService tradeService;

  @Async
  public void testCreateOrder() {
    // omit business logic ...
  }
}
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TradeService {
  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }
}

在标有 @Async 注解的场景下,在添加启用异步注解(@EnableAsync)后,代理对象会通过 AOP 自动生成。以上代码运行会抛出 BeanCurrentlyInCreationException 异常。运行的大致流程如下图所示:
源码在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory 类的方法 doCreateBean 中,会判断第二级缓存 earlySingletonObjects 中的对象是否等于原始对象,方法判断部分的源码如下:

二级缓存存放的对象是 AOP 生成出来的代理对象,和原始对象不相等,所以抛出了循环依赖错误。如果细看源码的话,会发现如果二级缓存是空的话会直接返回(因为比较的对象都没有,根本无法校验了),就不会报循环依赖的错误了,默认情况下,Spring 是按照文件全路径递归搜索,按路径 + 文件名 排序,排序靠前先加载,所以我们只要调整这两个类名称,让方法标有 @Async 注解的类排序在后面即可。

第 ④ 种场景——构造器注入

构造器注入的场景很少,到目前为止我所接触过的公司项目和开源项目中还没遇到使用构造器注入的,虽然用得不多,但是需要知道 Spring 为什么不支持这种场景的循环依赖,构造器注入的示例代码如下:

@Service
public class OrderService {
  private TradeService tradeService;

  public OrderService(TradeService tradeService) {
    this.tradeService = tradeService;
  }

  public void testCreateOrder() {
    // omit business logic ...
  }
}
@Service
public class TradeService {
  private OrderService orderService;

  public TradeService(OrderService orderService) {
    this.orderService = orderService;
  }

  public void testCreateTrade() {
    // omit business logic ...
  }
}

构造器注入无法加入到第三级缓存当中,Spring 框架中的三级缓存在此场景下无用武之地,所以只能抛出异常,整体流程如下(虚线表示无法执行,为了直观也把下一步画出来了):

第 ⑤ 种场景——DependsOn 循环依赖

这种 DependsOn 循环依赖场景很少,一般情况下不怎么使用,了解一下会导致循环依赖的问题即可,@DependsOn 注解主要是用来指定实例化顺序的,示例代码如下:

@Service
@DependsOn("tradeService")
public class OrderService {
  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }
}
@Service
@DependsOn("orderService")
public class TradeService {
  @Autowired
  private OrderService orderService;

  public void testCreateTrade() {
    // omit business logic ...
  }
}

通过上文,我们知道,如果这里的类没有标注 @DependsOn 注解的话是可以正常运行的,因为 Spring 支持单例 setter 注入,但是加了示例代码的 @DependsOn 注解后会报循环依赖错误,原因是在类 org.springframework.beans.factory.support.AbstractBeanFactory 的方法 doGetBean() 中检查了 dependsOn 的实例是否有循环依赖,如果有循环依赖则抛出循环依赖异常,方法判断部分代码如下:

五、补充:三大缓存介绍

Spring中有三个缓存,用于存储单例的Bean实例,这三个缓存是彼此互斥的,不会针对同一个Bean的实例同时存储。 如果调用getBean,则需要从三个缓存中依次获取指定的Bean实例。读取顺序依次是一级缓存–>二级缓存–>三级缓存。

一级缓存:Map singletonObjects

第一级缓存的作用?
用于存储单例模式下创建的Bean实例(已经创建完毕)。
该缓存是对外使用的,指的就是使用Spring框架的程序员。
存储什么数据?
K:bean的名称
V:bean的实例对象(有代理对象则指的是代理对象,已经创建完毕)

第二级缓存:Map earlySingletonObjects

第二级缓存的作用?
用于存储单例模式下创建的Bean实例(该Bean被提前暴露的引用,该Bean还在创建中)。
该缓存是对内使用的,指的就是Spring框架内部逻辑使用该缓存。
为了解决第一个classA引用最终如何替换为代理对象的问题(如果有代理对象)请爬楼参考演示案例
存储什么数据?
K:bean的名称
V:bean的实例对象(有代理对象则指的是代理对象,该Bean还在创建中)

第三级缓存:Map

第三级缓存的作用?
通过ObjectFactory对象来存储单例模式下提前暴露的Bean实例的引用(正在创建中)。该缓存是对内使用的,指的就是Spring框架内部逻辑使用该缓存。此缓存是解决循环依赖最大的功臣

存储什么数据?
K:bean的名称
V:ObjectFactory,该对象持有提前暴露的bean的引用

六、总结

最后用一张图来总结下Spring如何解决循环依赖问题的:
Spring如何解决循环依赖问题


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/java/4710.html
喜欢 (3)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】