高级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. 代码重构:定期对代码进行重构,消除潜在的循环依赖问题。
相关推荐
字节渡客9 小时前
Redis键明明过期了,业务还在读到旧数据
数据库·redis·spring
daidaidaiyu9 小时前
ThingsBoard 集群的核心逻辑源码分析
java
我可能是个假开发10 小时前
FTP Unicode 文件名导致 `550` 的问题分析与解决方案
java·开发语言·spring
caoerzhong10 小时前
中小企业上 WMS 该先上哪几块:JeeWMS 开源 Java 仓库管理系统的分批上线清单
java·python·开源
【JAVA】玩家11 小时前
Spring核心原理全解析:从零到生产实战
java·后端·spring
Wang's Blog12 小时前
Java框架 SpringCloud 快速入门: 实现 Feign 最佳实践(抽取方式)
java·spring cloud
MandalaO_O12 小时前
IDEA 开发(快捷键 + 调试 + 序列化)
java·ide·intellij-idea
xiaoqiMikko13 小时前
JVM 线上排查实战(七):jps 看不见它、jstack 连不上它,可它明明活得好好的
java·jvm
狼爷14 小时前
Rust/Go/Java/Python/PHP 大比拼:负载下后端框架到底差多少?
java·后端·编程语言
念何架构之路14 小时前
zap扩展生态与总结
java·前端·数据库