Spring 循环依赖深度解密:从问题本质到三级缓存源码级解析

作为 Java 后端开发者,你一定遇到过这个异常:

java 复制代码
BeanCurrentlyInCreationException: Requested bean is currently in creation: Is there an unresolvable circular reference?

这就是 Spring 中最常见也最让人头疼的循环依赖问题 。两个 Bean 相互依赖,就像 "先有鸡还是先有蛋" 一样,让 Spring 陷入了无限递归的死局。很多开发者遇到这个问题时,只会简单地加个@Lazy注解解决,却不知道背后的原理。

面试时,循环依赖更是 100% 的必考题,面试官会层层深挖:

  • 什么是循环依赖?Spring 能解决所有的循环依赖吗?
  • Spring 是如何解决循环依赖的?为什么需要三级缓存?
  • 为什么构造方法注入的循环依赖无法解决?
  • 为什么加了@Async注解会导致循环依赖失效?
  • 如何排查和避免循环依赖问题?

这篇文章,我们就从问题本质→Spring 解决方案→三级缓存源码解析→各种场景解决方案四个维度,彻底搞懂 Spring 循环依赖。不仅会讲清楚 "怎么解决",更会讲明白 "为什么这么解决",让你看完既能轻松应对面试,又能解决实际项目中的循环依赖问题。

一、先搞懂:什么是循环依赖?

循环依赖,简单来说就是两个或多个 Bean 之间相互依赖,形成了一个闭环。最常见的就是 A 依赖 B,同时 B 也依赖 A。

1. 循环依赖的三种形式

循环依赖有三种常见的形式:

(1)直接循环依赖(最常见)

两个 Bean 直接相互依赖:

java 复制代码
@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}
(2)间接循环依赖

多个 Bean 形成一个依赖环:

java 复制代码
@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private C c;
}

@Service
public class C {
    @Autowired
    private A a;
}
(3)自身循环依赖

一个 Bean 依赖自己:

java 复制代码
@Service
public class A {
    @Autowired
    private A a;
}

2. 为什么循环依赖会成为问题?

要理解为什么循环依赖会导致异常,我们需要回顾一下 Bean 的创建流程:

  1. 实例化 Bean:调用构造方法创建一个空的对象
  2. 依赖注入:为 Bean 的属性注入依赖
  3. 初始化:执行初始化方法
  4. 完成创建:Bean 可以被使用了

当出现循环依赖时,创建流程会变成这样:

  1. Spring 开始创建 A,实例化 A 得到一个空对象
  2. 为 A 注入依赖 B,发现 B 还没有创建
  3. Spring 开始创建 B,实例化 B 得到一个空对象
  4. 为 B 注入依赖 A,发现 A 正在创建中
  5. Spring 无法继续,抛出BeanCurrentlyInCreationException异常

这就像两个人同时需要对方的帮助才能完成工作,结果两个人都卡在了等待对方的状态,永远无法完成。

二、Spring 能解决哪些循环依赖?

很多人以为 Spring 能解决所有的循环依赖,其实不然。Spring 只能解决特定场景下的循环依赖,对于其他场景,Spring 也无能为力。

1. Spring 能解决的循环依赖

Spring 只能解决单例 Bean 的 setter 方法注入 / 字段注入的循环依赖。

这是我们日常开发中最常见的场景,也是 Spring 默认支持的场景。对于这种循环依赖,Spring 会自动解决,不需要我们做任何额外的配置。

2. Spring 不能解决的循环依赖

Spring 无法解决以下三种场景的循环依赖:

(1)原型 Bean 的循环依赖

原型 Bean(scope="prototype")每次获取都会创建一个新的实例。对于原型 Bean 的循环依赖,Spring 会直接抛出异常。

原因:Spring 不会缓存原型 Bean 的实例,每次创建都是一个新的对象,无法提前暴露早期对象。

(2)构造方法注入的循环依赖

通过构造方法注入依赖的循环依赖,Spring 无法解决。

原因:构造方法是在实例化阶段执行的,必须在实例化时就提供依赖对象,无法提前暴露一个不完整的对象。

(3)@Async@Transactional注解导致的循环依赖

如果循环依赖中的某个 Bean 被@Async@Transactional注解修饰,可能会导致循环依赖失效,抛出异常。

原因:这些注解会生成代理对象,而代理对象的生成时机和三级缓存的工作机制可能会产生冲突。

3. 循环依赖支持情况总结表

场景 Spring 是否支持 原因
单例 Bean + 字段注入 /setter 注入 可以通过三级缓存提前暴露早期对象
单例 Bean + 构造方法注入 构造方法执行时必须提供完整的依赖对象
原型 Bean + 任何注入方式 原型 Bean 不会被缓存,无法提前暴露
带 @Async/@Transactional 的单例 Bean 代理对象生成时机与三级缓存冲突

三、Spring 解决循环依赖的核心:三级缓存

Spring 解决单例 Bean 循环依赖的核心机制是三级缓存。很多人知道 Spring 有三级缓存,但不知道每一级缓存的作用,以及为什么需要三级而不是两级。

三、Spring 解决循环依赖的核心:三级缓存

Spring 解决单例 Bean 循环依赖的核心机制是三级缓存。很多人知道 Spring 有三级缓存,但不知道每一级缓存的作用,以及为什么需要三级而不是两级。

1. 三级缓存的定义

Spring 在DefaultSingletonBeanRegistry类中定义了三级缓存:

java 复制代码
// 一级缓存:存放完全初始化好的Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存:存放早期的Bean对象(已经实例化,但还没有完成依赖注入和初始化)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

// 三级缓存:存放Bean的工厂对象,用于生成早期Bean的代理对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

三级缓存的作用分别是:

  • 一级缓存(singletonObjects):存放已经完全创建好的 Bean,也就是经历了实例化、依赖注入、初始化所有阶段的 Bean。我们从 Spring 容器中获取的 Bean 都来自这里。
  • 二级缓存(earlySingletonObjects):存放已经实例化,但还没有完成依赖注入和初始化的早期 Bean 对象。用于解决循环依赖。
  • 三级缓存(singletonFactories):存放 Bean 的工厂对象(ObjectFactory),用于在需要的时候生成早期 Bean 的代理对象。这是解决 AOP 代理循环依赖的关键。

2. 循环依赖的完整解决流程

我们用最经典的 A 依赖 B,B 依赖 A 的例子,一步步拆解 Spring 是如何通过三级缓存解决循环依赖的。

步骤 1:开始创建 A
  1. Spring 从一级缓存中查找 A,没有找到
  2. 将 A 标记为 "正在创建中"
  3. 实例化 A,调用构造方法得到一个空的 A 对象
  4. 将 A 的工厂对象放入三级缓存:singletonFactories.put("a", () -> getEarlyBeanReference(beanName, mbd, bean))
  5. 为 A 注入依赖 B,发现 B 还没有创建
步骤 2:开始创建 B
  1. Spring 从一级缓存中查找 B,没有找到
  2. 将 B 标记为 "正在创建中"
  3. 实例化 B,调用构造方法得到一个空的 B 对象
  4. 将 B 的工厂对象放入三级缓存
  5. 为 B 注入依赖 A,发现 A 正在创建中
步骤 3:从三级缓存中获取 A 的早期对象
  1. Spring 从一级缓存中查找 A,没有找到
  2. 从二级缓存中查找 A,没有找到
  3. 从三级缓存中找到 A 的工厂对象
  4. 调用工厂对象的getObject()方法,得到 A 的早期对象
  5. 将 A 的早期对象从三级缓存移动到二级缓存
  6. 将 A 的早期对象注入到 B 中
步骤 4:完成 B 的创建
  1. B 完成依赖注入
  2. B 执行初始化方法
  3. 将 B 从三级缓存移动到一级缓存
  4. 返回 B 对象
步骤 5:完成 A 的创建
  1. 将 B 注入到 A 中
  2. A 完成依赖注入
  3. A 执行初始化方法
  4. 将 A 从二级缓存移动到一级缓存
  5. 返回 A 对象

至此,循环依赖解决完成,A 和 B 都成功创建。

3. 为什么需要三级缓存?两级缓存行不行?

这是面试中最常问到的问题。很多人会问:为什么不直接把早期对象放到二级缓存,还要多一个三级缓存?

答案是:为了解决 AOP 代理的问题

如果 Bean 不需要被 AOP 代理,那么确实只需要两级缓存就够了。但如果 Bean 需要被 AOP 代理,那么我们暴露给其他 Bean 的应该是代理对象,而不是原始对象。

三级缓存中的ObjectFactory的作用就是:在需要的时候,判断这个 Bean 是否需要被代理,如果需要,就返回代理对象;如果不需要,就返回原始对象

如果没有三级缓存,我们就需要在实例化 Bean 之后立即生成代理对象,不管这个 Bean 是否会被其他 Bean 引用。这会导致不必要的代理生成,影响性能。

而三级缓存的设计,实现了延迟生成代理对象:只有当这个 Bean 真的被其他 Bean 引用,并且处于循环依赖的情况下,才会生成代理对象。否则,会在 Bean 初始化完成后再生成代理对象。

这就是 Spring 设计三级缓存的精妙之处:既解决了循环依赖问题,又保证了 AOP 的正确性,同时还兼顾了性能。

四、深入:为什么这些场景的循环依赖无法解决?

我们之前说过,Spring 无法解决构造方法注入、原型 Bean 和@Async注解导致的循环依赖。现在我们来深入分析一下为什么。

1. 为什么构造方法注入的循环依赖无法解决?

构造方法注入的循环依赖无法解决,根本原因是构造方法的执行时机

构造方法是在 Bean 实例化阶段执行的,必须在实例化时就提供所有的依赖对象。也就是说,要创建 A,必须先有 B;要创建 B,必须先有 A。这就形成了一个死锁,Spring 无法打破这个死锁。

而 setter 方法注入是在实例化之后执行的,Spring 可以先实例化一个空的对象,然后再注入依赖。这就是为什么 setter 方法注入的循环依赖可以被解决。

2. 为什么原型 Bean 的循环依赖无法解决?

原型 Bean 的循环依赖无法解决,根本原因是Spring 不会缓存原型 Bean 的实例

对于单例 Bean,Spring 会将创建好的 Bean 缓存到一级缓存中,整个应用生命周期内只会创建一次。而对于原型 Bean,每次调用getBean()都会创建一个新的实例,Spring 不会缓存它。

当出现原型 Bean 的循环依赖时,创建 A 需要 B,创建 B 需要 A,每次都会创建新的对象,形成无限递归,最终导致栈溢出。

3. 为什么 @Async 注解会导致循环依赖失效?

@Async注解会导致循环依赖失效,根本原因是代理对象的生成时机不同

普通的 AOP 代理是在BeanPostProcessor.postProcessAfterInitialization()方法中生成的,也就是在 Bean 初始化完成之后。而@Async注解的代理是在一个单独的AsyncAnnotationBeanPostProcessor中生成的,这个处理器的执行顺序比普通的 AOP 处理器更靠后。

当出现循环依赖时,Spring 会从三级缓存中获取早期对象。对于普通的 AOP 代理,三级缓存会正确地返回代理对象。但对于@Async注解的代理,此时代理还没有生成,三级缓存返回的是原始对象。

当 Bean 初始化完成后,AsyncAnnotationBeanPostProcessor会生成代理对象,替换掉原始对象。但此时其他 Bean 已经注入了原始对象,导致代理对象没有被正确使用,最终抛出异常。

五、各种循环依赖场景的解决方案

对于 Spring 无法自动解决的循环依赖,我们需要手动干预。下面是各种场景下的解决方案。

1. 构造方法注入循环依赖的解决方案

构造方法注入的循环依赖是最常见的无法自动解决的场景,有以下三种解决方案:

方案 1:使用 @Lazy 注解(最简单)

在其中一个构造方法的参数上添加@Lazy注解,延迟加载依赖。

java 复制代码
@Service
public class A {
    private final B b;

    public A(@Lazy B b) {
        this.b = b;
    }
}

@Service
public class B {
    private final A a;

    public B(A a) {
        this.a = a;
    }
}

原理:Spring 会为 B 生成一个代理对象,注入到 A 的构造方法中。当 A 真正使用 B 的时候,才会创建真实的 B 对象。这样就打破了循环依赖。

方案 2:改用 setter 方法注入

将构造方法注入改为 setter 方法注入,让 Spring 可以通过三级缓存解决循环依赖。

java 复制代码
@Service
public class A {
    private B b;

    @Autowired
    public void setB(B b) {
        this.b = b;
    }
}

@Service
public class B {
    private A a;

    @Autowired
    public void setA(A a) {
        this.a = a;
    }
}
方案 3:使用 ApplicationContext.getBean ()

在需要使用依赖的时候,手动从 ApplicationContext 中获取。

java 复制代码
@Service
public class A implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    private B b;

    public void doSomething() {
        if (b == null) {
            b = applicationContext.getBean(B.class);
        }
        b.doSomething();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

2. @Async 注解导致循环依赖的解决方案

如果循环依赖中的某个 Bean 被@Async注解修饰,可以使用以下解决方案:

方案 1:将 @Async 注解移到单独的类中

这是最推荐的解决方案。将异步方法提取到一个单独的类中,避免循环依赖。

错误示例

java 复制代码
@Service
public class A {
    @Autowired
    private B b;

    @Async
    public void asyncMethod() {
        // 异步方法
    }
}

@Service
public class B {
    @Autowired
    private A a;
}

正确示例

java 复制代码
@Service
public class A {
    @Autowired
    private B b;

    @Autowired
    private AsyncService asyncService;

    public void doSomething() {
        asyncService.asyncMethod();
    }
}

@Service
public class AsyncService {
    @Async
    public void asyncMethod() {
        // 异步方法
    }
}

@Service
public class B {
    @Autowired
    private A a;
}
方案 2:使用 @Lazy 注解

在依赖注入的地方添加@Lazy注解。

java 复制代码
@Service
public class A {
    @Autowired
    @Lazy
    private B b;

    @Async
    public void asyncMethod() {
        // 异步方法
    }
}

3. 原型 Bean 循环依赖的解决方案

原型 Bean 的循环依赖几乎没有完美的解决方案,最好的方式是重构代码,避免原型 Bean 之间的循环依赖

如果确实需要使用原型 Bean 并且存在循环依赖,可以使用@Lazy注解延迟加载,或者手动从 ApplicationContext 中获取。

六、最佳实践:如何避免循环依赖?

虽然 Spring 能解决大部分循环依赖问题,但循环依赖本身就是一种不好的代码设计。它会导致代码耦合度高、难以理解、难以测试。

最好的解决方案是从根源上避免循环依赖。以下是一些避免循环依赖的最佳实践:

1. 合理设计代码结构

遵循单一职责原则,每个类只负责一个功能。如果两个类相互依赖,说明它们的职责划分可能不合理,需要重新设计。

2. 使用分层架构

严格遵循分层架构:Controller 层调用 Service 层,Service 层调用 Repository 层,同层之间尽量不要相互调用。

3. 提取公共代码

如果两个类都需要对方的功能,可以将公共功能提取到一个单独的工具类或服务类中。

4. 使用事件驱动模式

使用 Spring 的事件机制,通过发布和监听事件来解耦类之间的依赖关系。

java 复制代码
// 发布事件
@Service
public class A {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void doSomething() {
        // 执行业务逻辑
        eventPublisher.publishEvent(new AEvent());
    }
}

// 监听事件
@Service
public class B {
    @EventListener
    public void handleAEvent(AEvent event) {
        // 处理事件
    }
}

5. 定期检查循环依赖

在开发过程中,定期检查代码中的循环依赖,及时重构。可以使用 IDE 的依赖分析工具,或者 Spring Boot Actuator 的beans端点来查看 Bean 之间的依赖关系。

七、常见误区纠正

  1. 误区 :Spring 能解决所有的循环依赖。 纠正:Spring 只能解决单例 Bean 的 setter 方法注入 / 字段注入的循环依赖,无法解决构造方法注入、原型 Bean 和 @Async 注解导致的循环依赖。

  2. 误区 :三级缓存是为了解决循环依赖而设计的。 纠正:三级缓存的主要目的是为了解决 AOP 代理的问题,循环依赖的解决只是它的一个副作用。

  3. 误区 :使用 @Lazy 注解是解决循环依赖的最佳方案。 纠正:@Lazy 注解只是一种权宜之计,它并没有真正解决循环依赖,只是延迟了依赖的创建。最好的方案是重构代码,从根源上避免循环依赖。

  4. 误区 :循环依赖只会在两个 Bean 之间发生。 纠正:循环依赖可以发生在多个 Bean 之间,形成一个依赖环,比如 A→B→C→A。

八、高频面试题解答

  1. 问:什么是循环依赖?Spring 能解决哪些循环依赖? 答:循环依赖是指两个或多个 Bean 之间相互依赖,形成了一个闭环。Spring 只能解决单例 Bean 的 setter 方法注入 / 字段注入的循环依赖,无法解决构造方法注入、原型 Bean 和 @Async 注解导致的循环依赖。

  2. 问:Spring 是如何解决循环依赖的? 答:Spring 通过三级缓存机制解决循环依赖。三级缓存分别是 singletonObjects(存放完全初始化好的 Bean)、earlySingletonObjects(存放早期 Bean 对象)和 singletonFactories(存放 Bean 的工厂对象)。当出现循环依赖时,Spring 会提前暴露早期 Bean 对象,让其他 Bean 可以注入。

  3. 问:为什么需要三级缓存?两级缓存行不行? 答:为了解决 AOP 代理的问题。如果 Bean 需要被 AOP 代理,那么暴露给其他 Bean 的应该是代理对象,而不是原始对象。三级缓存中的 ObjectFactory 可以在需要的时候生成代理对象,实现延迟生成代理,兼顾了性能和正确性。

  4. 问:为什么构造方法注入的循环依赖无法解决? 答:因为构造方法是在实例化阶段执行的,必须在实例化时就提供所有的依赖对象。要创建 A 必须先有 B,要创建 B 必须先有 A,形成了死锁,Spring 无法打破。

  5. 问:为什么 @Async 注解会导致循环依赖失效? 答:因为 @Async 注解的代理是在一个单独的处理器中生成的,执行顺序比普通 AOP 处理器更靠后。当出现循环依赖时,三级缓存返回的是原始对象,而不是代理对象,导致代理没有被正确使用。

  6. 问:如何避免循环依赖? 答:最好的方式是从根源上避免,包括合理设计代码结构、使用分层架构、提取公共代码、使用事件驱动模式等。如果确实无法避免,可以使用 @Lazy 注解、setter 方法注入等方式解决。

九、总结

循环依赖是 Spring 中最常见的问题之一,也是面试的高频考点。理解循环依赖的本质和 Spring 的解决方案,不仅能帮助我们解决实际开发中的问题,更能让我们深入理解 Spring Bean 的生命周期和 AOP 的实现原理。

回顾一下全文的核心内容:

  • 循环依赖是指多个 Bean 之间相互依赖形成闭环
  • Spring 通过三级缓存机制解决单例 Bean 的 setter 注入循环依赖
  • 三级缓存的设计主要是为了解决 AOP 代理的问题
  • Spring 无法解决构造方法注入、原型 Bean 和 @Async 注解导致的循环依赖
  • 最好的解决方案是从根源上避免循环依赖,合理设计代码结构

记住:循环依赖本身就是一种代码坏味道。当你遇到循环依赖时,首先应该思考的是如何重构代码,而不是如何用技术手段去解决它。好的代码设计应该是低耦合、高内聚的,不应该出现循环依赖。

相关推荐
RyFit2 小时前
SpringAI 常见问题及解决方案大全
java·ai
石山代码3 小时前
C++ 内存分区 堆区
java·开发语言·c++
绝知此事3 小时前
【算法突围 01】线性结构与哈希表:后端开发的收纳术
java·数据结构·算法·面试·jdk·散列表
无风听海3 小时前
C# 隐式转换深度解析
java·开发语言·c#
一只大袋鼠4 小时前
Git 进阶(二):分支管理、暂存栈、远程仓库与多人协作
java·开发语言·git
德思特5 小时前
从 Dify 配置页理解 RAG 的重要参数
java·人工智能·llm·dify·rag
YOU OU5 小时前
Spring IoC&DI
java·数据库·spring
один but you5 小时前
从可变参数到 emplace:现代 C++ 性能优化的核心组合
java·开发语言
IT_陈寒5 小时前
Redis缓存击穿把我整不会了,原来还有这手操作
前端·人工智能·后端