当前位置: 首页 > news >正文

【2025最近Java面试八股】Spring中循环依赖的问题?怎么解决的?

1. 什么是循环依赖?

在Spring框架中,循环依赖是指两个或多个bean之间相互依赖,形成了一个循环引用的情况。如果不加以处理,这种情况会导致应用程序启动失败。导致 Spring 容器无法完成依赖注入。
例如:

@Service
public class A {@Autowiredprivate B b;
}@Service
public class B {@Autowiredprivate A a;
}

此时,A 依赖 BB 又依赖 A,Spring 无法确定先初始化哪个 Bean。

要解决循环依赖问题的限制

Spring解决循环依赖是有一定限制的:
首先就是要求互相依赖的Bean必须要是单例的Bean;

        为什么呢?Spring循环依赖的解决方案主要是通过对象的提前暴露来实现的。当一个对象在创建过程中需要引用到另一个正在创建的对象时,Spring会先提前暴露一个尚未完全初始化的对象实例,以解决循环依赖的问题。这个尚未完全初始化的对象实例就是半成品对象。

在 Spring 容器中,单例对象的创建和初始化只会发生一次,并且在容器启动时就完成了。这意味着,在容器运行期间,单例对象的依赖关系不会发生变化。因此,可以通过提前暴露半成品对象的方式来解决循环依赖的问题。

相比之下,原型对象的创建和初始化可以发生多次,并且可能在容器运行期间动态地发生变化。因此,对于原型对象,提前暴露半成品对象并不能解决循环依赖的问题,因为在后续的创建过程中,可能会涉及到不同的原型对象实例,无法像单例对象那样缓存并复用半成品对象。

另外就是依赖注入的方式不能都是构造函数注入的方式

为什么呢?Spring无法解决构造函数的循环依赖,是因为在对象实例化过程中,构造函数是最先被调用的而此时对象还未完成实例化,无法注入一个尚未完全创建的对象,因此Spring容器无法在构造函数注入中实现循环依赖的解决,像下面这样

@Component

public class ClassA {

        private final ClassB classB;

        @Autowired

        public ClassA(@Lazy ClassB classB) {

                this.classB = classB;

        }

        // ...

}

@Component

public class ClassB {

        private final ClassA classA;

        @Autowired

        public ClassB(ClassA classA) {

                this.classA = classA;

         }

        // ...

}

但是这样可以通过一些方法解决的

1、重新设计,彻底消除循环依赖

循环依赖,一般都是设计不合理导致的,可以从根本上做一些重构,来彻底解决,

2、改成非构造器注入

可以改成setter注入或者字段注入。

3、使用@Lazy解决(解决构造器循环依赖的)
首先要知道 Spring利用三级缓存是无法解决构造器注入这种循环依赖的。

@Lazy 是Spring框架中的一个注解,用于延迟一个bean的初始化,直到它第一次被使用。在默认情况下,Spring容器会在启动时创建并初始化所有的单例bean。这意味着,即使某个bean直到很晚才被使用,或者可能根本不被使用,它也会在应用启动时被创建。@Lazy 注解就是用来改变这种行为的。

也就是说,当我们使用 @Lazy 注解时,Spring容器会在需要该bean的时候才创建它,而不是在启动时。这意味着如果两个bean互相依赖,可以通过延迟其中一个bean的初始化来打破依赖循环。

缺点:过度使用 @Lazy 可能会导致应用程序的行为难以预测和跟踪,特别是在涉及多个依赖和复杂业务逻辑的情况下。

下面是一些例子

@Lazy 可以用在bean的定义上或者注入时。以下是一些使用示例:

@Component

@Lazy

public class LazyBean {

// ...

}

在这种情况下,LazyBean 只有在首次被使用时才会被创建和初始化。

@Component

public class SomeClass {

        private final LazyBean lazyBean;

        @Autowired

        public SomeClass(@Lazy LazyBean lazyBean) {

                this.lazyBean = lazyBean;

        }

}

在这里,即使SomeClass在容器启动时被创建,LazyBean也只会在SomeClass实际使用LazyBean时才被初始化。


2. Spring 如何检测循环依赖?依赖三级缓存机制

Spring 在 创建 Bean 的流程 中会检查循环依赖,主要通过 三级缓存(3-level cache) 机制:

  1. 一级缓存(Singleton Objects)
    存放 完全初始化好的 Bean(成品对象)。

  2. 二级缓存(Early Singleton Objects)
    存放 半成品 Bean(已实例化但未完成属性注入)。

    而当一个对象只进行了实例化,但是还没有进行初始化时,我们称之为半成品对象。所以,所谓半成品对象,其实只是 bean 对象的一个空壳子,还没有进行属性注入和初始化。
  3. 三级缓存(Singleton Factories)
    存放 Bean 的工厂对象(用于生成代理对象,如 AOP 场景)。

如果 Spring 发现某个 Bean 正在创建中(存在于二级缓存),但又再次被依赖,则判定为循环依赖。


3. Spring 如何解决循环依赖?spring提供了一种三级缓存的机制

前面说过Spring 的三级缓存仅能解决 单例(Singleton)作用域 且 通过属性注入(@Autowired) 的循环依赖,核心步骤如下:

首先,Spring中Bean的创建过程其实可以分成两步,第一步叫做实例化,第二步叫做初始化

具体流程如下:

(1) 创建 Bean A 的流程
  1. 实例化 A(调用构造函数,生成原始对象)。

  2. 将 A 的工厂对象放入三级缓存(用于后续可能的 AOP 代理)。

  3. 注入 A 的依赖(发现需要 B)。

  4. 去容器中获取 B(触发 B 的创建)。

(2) 创建 Bean B 的流程
  1. 实例化 B(生成原始对象)。

  2. 将 B 的工厂对象放入三级缓存

  3. 注入 B 的依赖(发现需要 A)。

  4. 从三级缓存获取 A 的工厂,生成 A 的早期引用(可能是代理对象,半成品)并放入二级缓存。

  5. B 完成属性注入,变成一个完整 Bean,放入一级缓存。

(3) 回到 A 的创建流程
  1. 从二级缓存拿到 B 的早期引用(半成品),注入到 A。

  2. A 完成初始化,从二级缓存移除,放入一级缓存。

最终,A 和 B 都成功创建,且互相持有对方的代理或真实对象。

以下是DefaultSingletonBeanRegistry#getSingleton方法,代码中,包括一级缓存、二级缓存、三级缓存的处理逻辑,该方法是获取bean的单例实例对象的核心方法:

@Nullable

protected Object getSingleton(String beanName, boolean allowEarlyReference) {

        // 首先从一级缓存中获取bean实例对象,如果已经存在,则直接返回

        Object singletonObject = this.singletonObjects.get(beanName);

                if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {

                // 如果一级缓存中不存在bean实例对象,而且当前bean正在创建中,则从二级缓                存中获取bean实例对象

                        singletonObject = this.earlySingletonObjects.get(beanName);

                        if (singletonObject == null && allowEarlyReference) {

                        // 如果二级缓存中也不存在bean实例对象,并且允许提前引用,则需要在锁定一级缓存之前,

                        // 先锁定二级缓存,然后再进行一系列处理

synchronized (this.singletonObjects) {

                        // 进行一系列安全检查后,再次从一级缓存和二级缓存中获取bean实例对象

singletonObject = this.singletonObjects.get(beanName);

                                if (singletonObject == null) {

                                        singletonObject = this.earlySingletonObjects.get(beanName);

                                                if (singletonObject == null) {

                                                        // 如果二级缓存中也不存在bean实例对象,则从三级缓存中获取bean的ObjectFactory,并创建bean实例对象

                                                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);

                                                if (singletonFactory != null) {

                                                        singletonObject = singletonFactory.getObject();

                                                        // 将创建好的bean实例对象存储到二级缓存中

                                                        this.earlySingletonObjects.put(beanName, singletonObject);

                                                        // 从三级缓存中移除bean的ObjectFactory

                                                        this.singletonFactories.remove(beanName);

                                                  }

                                          }

                                }

                        }

                }

        }

return singletonObject;

}

Spring解决循环依赖一定需要三级缓存吗?()

上面的流程可以看见只用到了Spring二级缓存就能解决依赖注入的问题。

其实,在大多数简单的循环依赖场景(没有AOP代理)中,二级缓存(Early Singleton Objects) 已经足够解决问题。但 Spring 仍然使用 三级缓存(Singleton Factories,存放 Bean 的工厂对象(用于生成代理对象,如 AOP 场景),主要是为了处理 AOP 代理 等特殊情况,确保返回的 Bean 是经过完整代理增强的对象。AOP又是Spring中很重要的一个特性,代理不能忽略。

所以三级缓存(Singleton Factories)的核心作用是 处理 AOP 代理,确保返回的 Bean 是代理对象而非原始对象。

(1)AOP 代理的问题
如果 Bean 需要被代理(如 @Transactional、@Async),Spring 不能直接返回原始对象,而是返回代理对象。

代理对象的生成时机:需要在 Bean 初始化完成后(即 postProcessAfterInitialization 阶段)。

(2)第三级缓存的作用
三级缓存存储的是 ObjectFactory,它可以在需要时生成代理对象。

流程示例:

实例化 A(原始对象)。

将 A 的 ObjectFactory 放入三级缓存(而非原始对象)。

注入 A 的依赖时,调用 ObjectFactory.getObject():

如果 A 需要代理,则返回代理对象;

如果不需要代理,则返回原始对象。

最终放入二级缓存的是 代理对象(或原始对象),而非直接暴露原始对象。

(3)如果没有三级缓存
如果直接将原始对象放入二级缓存,后续 AOP 代理无法替换它,导致 注入的是原始对象而非代理,可能引发问题(如事务失效)。


4. 哪些情况是三级缓存无法解决循环依赖?
场景原因
构造器注入(Constructor Injection)Spring 必须先完成构造器调用,无法提前暴露半成品 Bean。,可以使用@Lazy
原型(Prototype)作用域

Spring 不缓存原型 Bean,无法通过三级缓存机制解决。

提一点

对于原型对象,如果要解决循环依赖问题,要维护到底是哪两个对象之间的循环依赖,解决成本变高,而且循环依赖本来就不对,所以spring不支持。

如果检测到原型 Bean 的循环依赖,Spring 会直接报错:

org.springframework.beans.factory.BeanCurrentlyInCreationException: 
Error creating bean with name 'A': Requested bean is currently in creation: 
Is there an unresolvable circular reference?
@Async/@Transactional 等代理类

如果循环依赖涉及 AOP 代理,可能因代理生成时机(

1:代理对象生成时机冲突,注入的可能是原始对象而非代理

2:构造器注入 + AOP 代理    无法提前暴露半成品 Bean

3:原型 Bean + AOP 代理    原型 Bean 无法缓存半成品

问题导致失败(需用 @Lazy

一些极为特殊的情况。最好使用@Lazy,避免在复杂代理场景(如 @Transactional + @Async)中使用循环依赖


5. 如何避免或修复循环依赖?
(1) 代码设计层面
  • 避免双向依赖:重构代码,使用 单向依赖 或 接口隔离

  • 提取公共逻辑:将共用逻辑抽到第三个 Bean 中。

(2) Spring 提供的解决方案
  1. 使用 @Lazy 延迟加载
    在其中一个依赖上添加 @Lazy,让 Spring 暂时不注入真实对象,而是注入一个代理。

    @Service
    public class A {@Autowired@Lazy  // 延迟加载 Bprivate B b;
    }
  2. 改用 Setter/Field 注入
    替换构造器注入为属性注入:

    @Service
    public class A {private B b;@Autowired  // Setter 注入public void setB(B b) { this.b = b; }
    }
  3. 使用 ApplicationContext 手动获取 Bean
    在需要时再获取依赖(不推荐,破坏 IoC 设计):

    @Service
    public class A {@Autowiredprivate ApplicationContext context;public void doSomething() {B b = context.getBean(B.class); // 使用时再获取}
    }

6. 总结
要点说明
可解决的循环依赖单例 Bean + 属性注入(@Autowired)
Spring不可自动解决的循环依赖构造器注入、原型 Bean、某些 AOP 代理场景
解决方案@Lazy、Setter 注入、代码重构
Spring 底层机制三级缓存(Singleton Objects、Early Singleton Objects、Singleton Factories)

最佳实践

  • 优先通过 代码设计 避免循环依赖。

  • 必要时使用 @Lazy 或调整注入方式。

  • 避免在复杂项目中滥用循环依赖,降低维护成本。

相关文章:

  • 深度理解linux系统—— 进程概念
  • 端到端算法在SLAM中的应用:从理论到实践全解析
  • PlatformIO 入门学习笔记(一):背景了解
  • vue3项目中模拟AI的深度思考功能2.0
  • 2025 VSCode中如何进行dotnet开发环境配置完整教程
  • 【Java学习笔记】类与对象
  • 【文心快码】确实有点东西!
  • STM32标准库和HAL库SPI发送数据的区别-即SPI_I2S_SendData()和HAL_SPI_Transmit()互换
  • 计算机网络笔记(十四)——3.1数据链路层的几个共同问题
  • 03.使用spring-ai玩转MCP
  • ALTER TABLE 删除DROP表列的报错: 因为有一个或多个对象访问此列
  • 4.27 JavaScript核心语法+事件监听
  • 如何通过git删除某个文件的历史提交记录
  • 类-python
  • FISCO BCOS 智能合约开发详解
  • AlexNet网络搭建
  • 麒麟系统通过 Service 启动 JAR 包的完整指南
  • Lua 第12部分 日期和时间
  • Maven概述
  • 使用 Playwright 构建高效爬虫:原理、实战与最佳实践
  • 日韩 “打头阵”与美国贸易谈判,汽车、半导体产业忧虑重重
  • 网警侦破特大“刷量引流”网络水军案:涉案金额达2亿余元
  • 香港警务处高级助理处长叶云龙升任警务处副处长(行动)
  • 程璧“自由生长”,刘卓辉“被旋律牵着走”
  • 格力电器去年净利增长一成:消费电器营收下滑4%,一季度净利增长26%
  • 仲裁法修订草案二审稿拟增加规定规制虚假仲裁