高级java每日一道面试题-2025年01月13日-框架篇[Spring篇]-Spring 是怎么解决循环依赖的?

如果有遗漏,评论区告诉我进行补充

面试官: Spring 是怎么解决循环依赖的?

我回答:

在Java高级面试中,Spring框架如何解决循环依赖是一个重要且常见的问题。以下是对Spring解决循环依赖的详细解释:

循环依赖的定义与类型

循环依赖是指两个或多个Bean之间互相依赖,形成一个闭环。在Spring框架中,循环依赖通常发生在依赖注入(Dependency Injection)过程中。循环依赖的类型主要包括构造函数循环依赖和属性(或Setter)循环依赖。

Spring 如何检测和解决循环依赖?

Spring 使用了多种策略来检测并解决循环依赖问题,主要包括以下几种方式:

单例 Bean 的三级缓存机制

Spring 容器为每个单例 Bean 维护了三个不同级别的缓存:

  • singletonObjects:存放已经完全初始化完成的 Bean 实例。
  • earlySingletonObjects:存放尚未完成初始化但已经被实例化的 Bean(即半成品 Bean)。这些 Bean 已经完成了构造函数注入,但是还没有完成属性注入和其他初始化方法调用。
  • singletonFactories:存放 FactoryBean 或者用于创建早期暴露对象的工厂方法。

当 Spring 检测到 A 和 B 之间的循环依赖时,它会按照如下步骤操作:

  1. 创建 Bean A :Spring 开始创建 Bean A,并将其放入 singletonFactories 中。
  2. 实例化 Bean A:接着实例化 Bean A 并设置其非循环依赖的属性。
  3. 提前暴露 Bean A :将 Bean A 提前暴露给其他 Bean 使用,此时 Bean A 被移入 earlySingletonObjects 缓存。
  4. 创建 Bean B:尝试创建 Bean B,在此过程中发现它依赖于 Bean A。
  5. 获取 Bean A :由于 Bean A 已经存在于 earlySingletonObjects 中,所以可以直接获取并设置到 Bean B 上。
  6. 继续初始化 Bean A :回到 Bean A 的初始化流程,设置它的剩余属性(包括对 Bean B 的引用),最后将 Bean A 移入 singletonObjects 缓存。
  7. 完成 Bean B 的初始化 :现在可以安全地完成 Bean B 的初始化,因为它已经获得了对 Bean A 的引用。
    通过这种方式,Spring 成功地打破了循环依赖,使得两个相互依赖的 Bean 都能正常初始化。
构造器注入 vs 字段/Setter 注入
  • 构造器注入:如果使用构造器注入,则在构造函数中传递所有必需的依赖项。在这种情况下,如果存在循环依赖,Spring 将抛出异常,因为无法同时满足两个 Bean 的构造需求。
  • 字段/Setter 注入:相比之下,字段注入或 Setter 方法注入允许 Spring 在实例化之后再设置依赖关系,这为解决循环依赖提供了可能。因此,推荐在需要支持循环依赖的情况下优先考虑字段或 Setter 注入。
@Lazy 注解

对于某些特定场景下的循环依赖问题,可以使用 @Lazy 注解延迟加载某个 Bean,直到真正需要它的时候才进行实例化。这样可以避免在启动阶段就触发循环依赖错误。

java 复制代码
@Component
public class ServiceA {
    private final ServiceB serviceB;

    @Autowired
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}
代理模式

Spring 还可以通过 CGLIB 动态代理的方式生成目标 Bean 的代理类,然后在代理类中实现对原始 Bean 的懒加载。这种方式适用于接口类型的 Bean,能够有效缓解循环依赖问题。

Spring无法解决的循环依赖情况

需要注意的是,Spring的循环依赖解决机制有一些限制:

  1. 原型作用域的Bean:对于原型作用域的Bean,由于每次请求都会创建一个新的Bean实例,因此无法使用缓存来解决循环依赖。
  2. 构造器注入的循环依赖:如果Bean的构造方法中存在循环依赖,Spring也无法解决。因为在构造方法中,Bean实例还未创建,无法放入缓存。

解决循环依赖的最佳实践

尽管Spring提供了解决循环依赖的机制,但在设计时仍应尽量避免出现循环依赖,因为循环依赖可能导致代码的可读性差,并且可能是设计上的问题。以下是一些解决循环依赖的最佳实践:

  1. 模块化:将代码拆分成独立的模块,使每个模块只负责一个功能,降低模块间的耦合度。
  2. 使用依赖注入:通过依赖注入,将依赖关系从代码中解耦,使得一个类不再直接依赖另一个类,而是依赖于一个接口或抽象类。
  3. 使用设计模式:利用设计模式(如观察者模式、中介者模式等)来帮助更好地组织代码,避免循环依赖的产生。
  4. 代码重构:定期对代码进行重构,消除潜在的循环依赖问题。
相关推荐
瓯雅爱分享2 小时前
Java+Vue构建的采购招投标一体化管理系统,集成招标计划、投标审核、在线竞价、中标公示及合同跟踪功能,附完整源码,助力企业实现采购全流程自动化与规范化
java·mysql·vue·软件工程·源代码管理
mit6.8245 小时前
[C# starter-kit] 命令/查询职责分离CQRS | MediatR |
java·数据库·c#
诸神缄默不语5 小时前
Maven用户设置文件(settings.xml)配置指南
xml·java·maven
任子菲阳5 小时前
学Java第三十四天-----抽象类和抽象方法
java·开发语言
学Linux的语莫6 小时前
机器学习数据处理
java·算法·机器学习
找不到、了6 小时前
JVM的即时编译JIT的介绍
java·jvm
西瓜er6 小时前
JAVA:Spring Boot 集成 FFmpeg 实现多媒体处理
java·spring boot·ffmpeg
你总是一副不开心的样子(´ . .̫ .6 小时前
一、十天速通Java面试(第三天)
java·面试·职场和发展·java面试
迎風吹頭髮7 小时前
UNIX下C语言编程与实践63-UNIX 并发 Socket 编程:非阻塞套接字与轮询模型
java·c语言·unix
我是华为OD~HR~栗栗呀7 小时前
23届考研-Java面经(华为OD)
java·c++·python·华为od·华为·面试