Note 04. 源码级深度剖析:循环依赖与三级缓存

Note 04. 依赖关系的疑难杂症:常见问题与解决方案

摘要:在前面的章节中,我们学习了如何注册 Bean、如何注入依赖、Bean 的生命周期是怎样的。但在实际开发中,依赖关系往往没有那么简单——一个接口有多个实现类该注入哪个?某个依赖可能不存在怎么办?两个 Bean 互相依赖怎么处理?如何控制 Bean 的创建顺序?本章将逐一解答这些 “疑难杂症”,帮助你在复杂的依赖场景中游刃有余。

本章学习路径

  1. 多 Bean 选择:掌握 @Primary、@Qualifier 等机制,解决 “一个接口多个实现” 的选择问题。
  2. 可选依赖:学会处理 “依赖可能不存在” 的场景,避免启动失败。
  3. 循环依赖:理解循环依赖的本质,知道如何识别和避免它。
  4. 顺序控制:掌握 @DependsOn、@Order 等注解,控制 Bean 的创建和执行顺序。
  5. 延迟加载:理解 @Lazy 的原理和适用场景。

4.1. 依赖注入中的 “选择困难症”:多个同类型 Bean

在前面的章节中,我们学习的依赖注入场景都比较简单:一个类型对应一个 Bean,Spring 自动找到并注入。但在实际项目中,情况往往没这么单纯——一个接口可能有多个实现类,每个实现类都是一个 Bean。这时候,Spring 就会陷入 “选择困难症”:你要的是哪一个?

4.1.1. 问题场景:一个接口多个实现

为什么会出现多个同类型的 Bean?

在面向接口编程的实践中,我们经常会定义一个接口,然后提供多个实现。这是一种良好的设计模式,它带来了灵活性和可扩展性。

举个例子:假设我们正在开发一个消息通知系统。通知可以通过多种渠道发送——邮件、短信、App 推送。按照面向接口编程的原则,我们会这样设计:

mermaid-diagram-2026-01-03-202547

这个设计非常合理:

  • 统一抽象:所有发送器都实现同一个接口,调用方不需要关心具体实现
  • 易于扩展:未来要加微信通知?再写一个 WechatSender 实现接口即可
  • 便于测试:可以轻松用 Mock 实现替换真实实现

代码实现

首先定义接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo.sender;

/**
* 消息发送器接口
* 定义了发送消息的统一契约
*/
public interface MessageSender {

/**
* 发送消息
* @param to 接收者(可以是邮箱、手机号、用户 ID 等,取决于具体实现)
* @param content 消息内容
*/
void send(String to, String content);
}

然后提供三个实现,每个都注册为 Spring Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.sender;

import org.springframework.stereotype.Component;

/**
* 邮件发送器
*/
@Component
public class EmailSender implements MessageSender {

@Override
public void send(String to, String content) {
// 实际项目中这里会调用邮件服务 API
System.out.println("[邮件] 发送到 " + to + ": " + content);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.sender;

import org.springframework.stereotype.Component;

/**
* 短信发送器
*/
@Component
public class SmsSender implements MessageSender {

@Override
public void send(String to, String content) {
// 实际项目中这里会调用短信服务 API
System.out.println("[短信] 发送到 " + to + ": " + content);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.sender;

import org.springframework.stereotype.Component;

/**
* App 推送发送器
*/
@Component
public class PushSender implements MessageSender {

@Override
public void send(String to, String content) {
// 实际项目中这里会调用推送服务 API
System.out.println("[推送] 发送到 " + to + ": " + content);
}
}

到目前为止,一切看起来都很美好。三个实现类都注册成了 Bean,等待被使用。

问题出现:Spring 不知道该注入哪个

现在,我们写一个通知服务,想要注入一个 MessageSender 来发送消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.service;

import com.example.demo.sender.MessageSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

@Autowired
private MessageSender messageSender; // 想注入一个消息发送器

public void notifyUser(String userId, String message) {
messageSender.send(userId, message);
}
}

看起来没问题,对吧?但当你启动应用时,会看到这样的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
***************************
APPLICATION FAILED TO START
***************************

Description:

Field messageSender in com.example.demo.service.NotificationService
required a single bean, but 3 were found:
- emailSender: defined in file [.../EmailSender.class]
- smsSender: defined in file [.../SmsSender.class]
- pushSender: defined in file [.../PushSender.class]

Action:

Consider marking one of the beans as @Primary, updating the consumer
to accept multiple beans, or using @Qualifier to identify the bean
that should be consumed

错误信息解读

这段错误信息非常清晰,让我们逐行理解:

错误信息含义
required a single bean, but 3 were found你要求注入一个 Bean,但我找到了 3 个
emailSender, smsSender, pushSender这是找到的 3 个 Bean 的名称
Consider marking one of the beans as @Primary建议一:用 @Primary 标记一个默认的
using @Qualifier to identify the bean建议二:用 @Qualifier 指定要哪个
accept multiple beans建议三:改成接收多个 Bean(List 注入)

Spring 的错误提示已经给出了解决方案的方向。接下来,我们逐一学习这些解决方案。

问题的本质

问题的本质是:当存在多个候选者时,Spring 需要一个 “决策依据” 来选择注入哪个。我们接下来学习的 @Primary@Qualifier 等注解,就是提供这个 “决策依据” 的方式。

4.1.2. 解决方案一:@Primary 标记默认首选

设计意图

@Primary 的设计意图是:在多个同类型 Bean 中,标记一个作为 “默认首选”。当注入时没有其他特殊指定,Spring 就会选择这个 “默认” 的。

这就像是在餐厅点菜时说 “来一份招牌菜”——你不需要知道具体是什么菜,餐厅会给你他们的 “默认推荐”。

使用方式

只需要在你希望作为默认的实现类上加上 @Primary 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.demo.sender;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary // 标记为首选,当有多个 MessageSender 时,默认使用这个
public class EmailSender implements MessageSender {

@Override
public void send(String to, String content) {
System.out.println("[邮件] 发送到 " + to + ": " + content);
}
}

其他实现类保持不变:

1
2
3
4
5
6
7
8
9
@Component  // 没有 @Primary
public class SmsSender implements MessageSender {
// ...
}

@Component // 没有 @Primary
public class PushSender implements MessageSender {
// ...
}

效果验证

现在 NotificationService 可以正常启动了:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class NotificationService {

@Autowired
private MessageSender messageSender; // 会注入 EmailSender

public void notifyUser(String userId, String message) {
System.out.println("准备发送通知...");
messageSender.send(userId, message);
}
}

@Primary 的适用场景

场景说明示例
有明确的 “默认” 实现大多数情况下使用某一个实现默认用邮件通知,特殊情况才用短信
测试环境替换用 Mock 实现替换真实实现测试时用 MockEmailSender 替换真实的
简化配置不想在每个注入点都指定90% 的地方都用同一个实现

@Primary 的局限性

@Primary 有一个明显的限制:只能标记一个 Bean 为首选

1
2
3
4
5
6
7
@Component
@Primary
public class EmailSender implements MessageSender { }

@Component
@Primary // ❌ 编译不会报错,但运行时会有问题
public class SmsSender implements MessageSender { }

如果多个 Bean 都标记了 @Primary,Spring 又会陷入 “选择困难”,因为它不知道哪个 “更首选”。

另外,如果你需要在 不同的地方注入不同的实现@Primary 就无能为力了。这时候需要用 @Qualifier

4.1.3. 解决方案二:@Qualifier 精确指定

设计意图

@Qualifier 的设计意图是:通过名称精确指定要注入哪个 Bean

如果说 @Primary 是 “给我默认的那个”,那 @Qualifier 就是 “我要那个叫 xxx 的”。

Bean 的名称从哪里来?

在使用 @Qualifier 之前,我们需要先了解 Bean 的命名规则。

当你用 @Component 注册一个 Bean 时,Spring 会自动给它一个名称。默认规则是:类名的首字母小写

类名默认 Bean 名称
EmailSenderemailSender
SmsSendersmsSender
PushSenderpushSender
UserServiceuserService
HTTPClientHTTPClient(注意:连续大写字母保持原样)
OAuth2ServiceOAuth2Service(注意:以大写字母开头的缩写保持原样)

你也可以在 @Component 中显式指定名称:

1
2
3
4
@Component("email")  // 显式指定名称为 "email"
public class EmailSender implements MessageSender {
// ...
}

基本用法

在注入点使用 @Qualifier 指定 Bean 名称:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class NotificationService {

@Autowired
@Qualifier("smsSender") // 明确指定:我要 smsSender 这个 Bean
private MessageSender messageSender;

public void notifyUser(String userId, String message) {
messageSender.send(userId, message);
// 输出: [短信] 发送到 user123: 你好!
}
}

在构造器注入中使用 @Qualifier

构造器注入是推荐的注入方式,@Qualifier 需要放在参数上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class NotificationService {

private final MessageSender primarySender;
private final MessageSender backupSender;

// 在构造器参数上使用 @Qualifier
public NotificationService(
@Qualifier("emailSender") MessageSender primarySender,
@Qualifier("smsSender") MessageSender backupSender) {
this.primarySender = primarySender;
this.backupSender = backupSender;
}

public void notifyUser(String userId, String message) {
try {
System.out.println("尝试通过主渠道发送...");
primarySender.send(userId, message);
} catch (Exception e) {
System.out.println("主渠道失败,切换到备用渠道...");
backupSender.send(userId, message);
}
}
}

@Qualifier 的问题:字符串没有编译时检查

@Qualifier 使用字符串来指定 Bean 名称,这带来了一个隐患:拼写错误只能在运行时发现

1
2
3
4
5
6
7
@Service
public class NotificationService {

@Autowired
@Qualifier("emialSender") // 😱 拼写错误!email 写成了 emial
private MessageSender messageSender;
}

这段代码编译完全没问题,但启动时会报错:

1
2
No qualifying bean of type 'MessageSender' available:
expected at least 1 bean which qualifies as autowire candidate.

在大型项目中,这种错误可能很难排查。为了解决这个问题,Spring 提供了更优雅的方案:自定义限定符注解。

4.1.4. 解决方案三:自定义限定符注解(类型安全)

设计意图

自定义限定符注解的设计意图是:用类型安全的注解替代字符串,让编译器帮我们检查错误

实现步骤

步骤一:定义自定义注解

为每种实现创建一个专门的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo.sender.qualifier;

import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

/**
* 邮件渠道限定符
* 用于标记和选择邮件发送器
*/
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier // 关键!这个元注解告诉 Spring 这是一个限定符
public @interface EmailChannel {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.demo.sender.qualifier;

import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

/**
* 短信渠道限定符
*/
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
public @interface SmsChannel {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.demo.sender.qualifier;

import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

/**
* 推送渠道限定符
*/
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
public @interface PushChannel {
}

步骤二:在 Bean 上使用自定义注解

1
2
3
4
5
6
7
8
9
@Component
@EmailChannel // 使用自定义注解标记
public class EmailSender implements MessageSender {

@Override
public void send(String to, String content) {
System.out.println("[邮件] 发送到 " + to + ": " + content);
}
}
1
2
3
4
5
6
7
8
9
@Component
@SmsChannel
public class SmsSender implements MessageSender {

@Override
public void send(String to, String content) {
System.out.println("[短信] 发送到 " + to + ": " + content);
}
}
1
2
3
4
5
6
7
8
9
@Component
@PushChannel
public class PushSender implements MessageSender {

@Override
public void send(String to, String content) {
System.out.println("[推送] 发送到 " + to + ": " + content);
}
}

步骤三:注入时使用自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class NotificationService {

private final MessageSender emailSender;
private final MessageSender smsSender;
private final MessageSender pushSender;

public NotificationService(
@EmailChannel MessageSender emailSender, // 类型安全!
@SmsChannel MessageSender smsSender, // IDE 会自动补全
@PushChannel MessageSender pushSender) { // 拼写错误会编译报错
this.emailSender = emailSender;
this.smsSender = smsSender;
this.pushSender = pushSender;
}

public void notifyByEmail(String userId, String message) {
emailSender.send(userId, message);
}

public void notifyBySms(String userId, String message) {
smsSender.send(userId, message);
}

public void notifyByPush(String userId, String message) {
pushSender.send(userId, message);
}
}

类型安全的优势

现在,如果你不小心拼错了注解名:

1
2
3
4
public NotificationService(
@EmialChannel MessageSender emailSender) { // 😱 拼写错误
// ...
}

编译器会立即报错:Cannot resolve symbol 'EmialChannel'。问题在编写代码时就被发现,而不是等到运行时。

三种方案对比

对比维度@Primary@Qualifier(“字符串”)自定义限定符注解
使用场景有一个默认实现需要精确选择需要精确选择且追求类型安全
类型安全❌ 字符串无编译检查✅ 注解有编译检查
IDE 支持⚠️ 有限✅ 完整的跳转、重构、补全
代码量最少较少较多(需要定义注解)
灵活性低(只能有一个默认)
推荐场景简单场景临时使用或少量使用多处使用、追求代码质量

4.1.5. 解决方案四:注入所有实现(List 和 Map)

前面的方案都是 “选择一个”,但有时候我们的需求是 “我全都要”——获取某个接口的所有实现。

场景一:同时通过所有渠道发送通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
public class BroadcastNotificationService {

private final List<MessageSender> allSenders;

// Spring 会自动注入所有 MessageSender 类型的 Bean
public BroadcastNotificationService(List<MessageSender> allSenders) {
this.allSenders = allSenders;

// 启动时打印,确认注入了哪些
System.out.println("已加载 " + allSenders.size() + " 个消息发送器:");
allSenders.forEach(sender ->
System.out.println(" - " + sender.getClass().getSimpleName())
);
}

/**
* 通过所有渠道广播消息
*/
public void broadcast(String userId, String message) {
System.out.println("开始广播消息到所有渠道...");

for (MessageSender sender : allSenders) {
try {
sender.send(userId, message);
} catch (Exception e) {
// 某个渠道失败不影响其他渠道
System.err.println(sender.getClass().getSimpleName()
+ " 发送失败: " + e.getMessage());
}
}

System.out.println("广播完成");
}
}

启动时的输出

1
2
3
4
已加载 3 个消息发送器:
- EmailSender
- SmsSender
- PushSender

调用 broadcast 方法的输出

1
2
3
4
5
开始广播消息到所有渠道...
[邮件] 发送到 user123: 重要通知!
[短信] 发送到 user123: 重要通知!
[推送] 发送到 user123: 重要通知!
广播完成

场景二:根据参数动态选择渠道

使用 Map 注入可以按名称索引所有实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class DynamicNotificationService {

private final Map<String, MessageSender> senderMap;

// Key 是 Bean 名称,Value 是 Bean 实例
public DynamicNotificationService(Map<String, MessageSender> senderMap) {
this.senderMap = senderMap;

System.out.println("可用的发送渠道: " + senderMap.keySet());
// 输出: 可用的发送渠道: [emailSender, smsSender, pushSender]
}

/**
* 根据指定的渠道发送消息
* @param channel 渠道名称(Bean 名称)
*/
public void notify(String channel, String userId, String message) {
MessageSender sender = senderMap.get(channel);

if (sender == null) {
throw new IllegalArgumentException(
"未知的发送渠道: " + channel + ",可用渠道: " + senderMap.keySet()
);
}

sender.send(userId, message);
}
}

控制 List 中 Bean 的顺序

默认情况下,List 中 Bean 的顺序是不确定的。如果顺序很重要(比如你想先尝试邮件,失败再尝试短信),可以使用 @Order 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@Order(1) // 顺序值越小,在 List 中越靠前
public class EmailSender implements MessageSender {
// ...
}

@Component
@Order(2)
public class SmsSender implements MessageSender {
// ...
}

@Component
@Order(3)
public class PushSender implements MessageSender {
// ...
}

现在 List 中的顺序就是:EmailSender → SmsSender → PushSender。

4.1.6. 本节小结

当一个接口有多个实现类都注册为 Bean 时,Spring 需要明确的 “选择指示” 才能完成注入。我们学习了四种解决方案:

方案核心思想适用场景
@Primary标记一个默认首选有明确的 “默认” 实现,大多数地方用同一个
@Qualifier按名称精确指定需要在不同地方注入不同实现
自定义限定符注解类型安全的精确指定多处使用,追求代码质量和 IDE 支持
List/Map 注入获取所有实现需要遍历所有实现,或动态选择

4.2. 可选依赖与条件依赖:不是所有依赖都必须存在

在某些场景下,一个 Bean 的依赖可能不是必须的——它可能存在,也可能不存在。如果依赖不存在就直接启动失败,显然不够灵活。

4.2.1. @Autowired(required = false):依赖可能不存在

默认情况下,@Autowiredrequired 属性是 true,意味着依赖必须存在,否则启动失败。

将其设置为 false,可以让依赖变成可选的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class ReportService {

@Autowired(required = false) // 可选依赖
private EmailSender emailSender;

public void generateReport() {
String report = "这是报表内容...";

// 如果邮件发送器存在,就发送邮件
if (emailSender != null) {
emailSender.send("admin@example.com", report);
} else {
System.out.println("邮件发送器未配置,跳过邮件通知");
}
}
}

构造器注入中的可选依赖

构造器注入不能直接使用 required = false(因为构造器参数不能为 null)。需要换一种方式:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class ReportService {

private final EmailSender emailSender;

// 使用 @Autowired(required = false) 在构造器上不太优雅
// 推荐使用 Optional 或 ObjectProvider(见下文)
public ReportService(@Autowired(required = false) EmailSender emailSender) {
this.emailSender = emailSender;
}
}

4.2.2. Optional 包装:更现代的可选依赖处理

Java 8 引入的 Optional 类型可以更优雅地处理可选依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class ReportService {

private final Optional<EmailSender> emailSender;

public ReportService(Optional<EmailSender> emailSender) {
this.emailSender = emailSender;
}

public void generateReport() {
String report = "这是报表内容...";

// 使用 Optional 的 API 处理
emailSender.ifPresentOrElse(
sender -> sender.send("admin@example.com", report),
() -> System.out.println("邮件发送器未配置,跳过邮件通知")
);
}
}

Optional 的优势

  1. 语义清晰:从类型上就能看出这是一个可选依赖
  2. API 丰富ifPresentorElsemap 等方法让处理更优雅
  3. 避免 NPE:强制你处理 “不存在” 的情况

4.2.3. ObjectProvider:延迟获取与安全访问

ObjectProvider 是 Spring 提供的一个接口,它比 Optional 更强大,支持延迟获取、多实例获取等功能。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class ReportService {

private final ObjectProvider<EmailSender> emailSenderProvider;

public ReportService(ObjectProvider<EmailSender> emailSenderProvider) {
this.emailSenderProvider = emailSenderProvider;
}

public void generateReport() {
String report = "这是报表内容...";

// 安全获取,不存在时返回 null
EmailSender sender = emailSenderProvider.getIfAvailable();
if (sender != null) {
sender.send("admin@example.com", report);
}

// 或者使用 Lambda 表达式
emailSenderProvider.ifAvailable(
s -> s.send("admin@example.com", report)
);
}
}

ObjectProvider 的常用方法

方法说明
getIfAvailable()如果 Bean 存在则返回,否则返回 null
getIfAvailable(Supplier)如果 Bean 存在则返回,否则返回 Supplier 提供的默认值
ifAvailable(Consumer)如果 Bean 存在则执行 Consumer
getIfUnique()如果恰好有一个 Bean 则返回,否则返回 null
stream()返回所有匹配 Bean 的 Stream
orderedStream()返回按 @Order 排序的 Stream

ObjectProvider vs Optional

对比维度OptionalObjectProvider
来源Java 标准库Spring 框架
延迟加载❌ 注入时就确定✅ 调用时才获取
多实例支持✅ stream()、orderedStream()
唯一性检查✅ getIfUnique()
默认值支持✅ orElse()✅ getIfAvailable(Supplier)

推荐:如果只是简单的可选依赖,用 Optional;如果需要延迟加载或多实例支持,用 ObjectProvider

4.2.4. @ConditionalOnBean:根据其他 Bean 决定是否创建

有时候,我们希望一个 Bean 只在另一个 Bean 存在时才创建。这在编写自动配置时特别有用。

场景示例

假设我们有一个 EmailNotificationService,它只有在 EmailSender 存在时才有意义:

1
2
3
4
5
6
7
8
9
@Configuration
public class NotificationConfig {

@Bean
@ConditionalOnBean(EmailSender.class) // 只有当 EmailSender 存在时才创建
public EmailNotificationService emailNotificationService(EmailSender emailSender) {
return new EmailNotificationService(emailSender);
}
}

常用的条件注解

注解条件
@ConditionalOnBean指定的 Bean 存在时
@ConditionalOnMissingBean指定的 Bean 不存在时
@ConditionalOnClass指定的类在类路径上时
@ConditionalOnMissingClass指定的类不在类路径上时
@ConditionalOnProperty指定的配置属性满足条件时

实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class CacheConfig {

// 如果引入了 Redis 依赖,就使用 Redis 缓存
@Bean
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
@ConditionalOnProperty(name = "cache.type", havingValue = "redis")
public CacheManager redisCacheManager() {
System.out.println("使用 Redis 缓存");
return new RedisCacheManager();
}

// 如果没有 Redis,就使用本地缓存
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager localCacheManager() {
System.out.println("使用本地缓存");
return new ConcurrentMapCacheManager();
}
}

4.3. 循环依赖:一个应该避免的设计问题

循环依赖是 Spring 开发中一个经典的话题。虽然 Spring 在某些情况下可以自动解决循环依赖,但从 Spring Boot 2.6 开始,默认禁止循环依赖。这意味着:循环依赖是一个应该避免的设计问题,而不是依赖框架来解决的问题。

4.3.1. 什么是循环依赖:A 依赖 B,B 依赖 A

最简单的循环依赖

1
2
3
4
5
6
7
8
9
10
@Service
public class ServiceA {

@Autowired
private ServiceB serviceB; // A 依赖 B

public void doSomething() {
serviceB.doSomethingElse();
}
}
1
2
3
4
5
6
7
8
9
10
@Service
public class ServiceB {

@Autowired
private ServiceA serviceA; // B 依赖 A

public void doSomethingElse() {
serviceA.doSomething();
}
}

这就形成了一个循环:A → B → A → B → …

更复杂的循环依赖

循环依赖不一定是两个类之间的直接循环,也可能是多个类形成的环:

image-20260103204012672

A → B → C → A,三个类形成了一个循环。

Spring Boot 2.6+ 的默认行为

从 Spring Boot 2.6 开始,如果检测到循环依赖,应用会 启动失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
| serviceA defined in file [ServiceA.class]
↑ ↓
| serviceB defined in file [ServiceB.class]
└─────┘

Action:

Relying upon circular references is discouraged and they are prohibited by default.
Update your application to remove the dependency cycle between beans.
As a last resort, it may be possible to break the cycle automatically by setting
spring.main.allow-circular-references to true.

4.3.2. Spring 如何解决循环依赖(简述原理)

在 Spring Boot 2.6 之前(或手动开启 allow-circular-references),Spring 可以解决 部分场景 的循环依赖。了解其原理有助于理解为什么某些情况下循环依赖无法解决。

Spring 能解决的情况:字段注入 / Setter 注入的单例 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class ServiceA {

@Autowired
private ServiceB serviceB; // 字段注入
}

@Service
public class ServiceB {

@Autowired
private ServiceA serviceA; // 字段注入
}

解决原理(简化版)

Spring 使用 “三级缓存” 来解决这个问题。简单来说:

  1. Spring 开始创建 ServiceA
  2. ServiceA 的构造函数执行完毕,得到一个 “半成品” 对象(还没注入依赖)
  3. Spring 把这个 “半成品” 放入缓存
  4. Spring 发现 ServiceA 需要 ServiceB,开始创建 ServiceB
  5. ServiceB 的构造函数执行完毕,得到 “半成品”
  6. Spring 发现 ServiceB 需要 ServiceA,从缓存中取出 ServiceA 的 “半成品” 注入
  7. ServiceB 创建完成
  8. 回到 ServiceA,把 ServiceB 注入进去
  9. ServiceA 创建完成

mermaid-diagram-2026-01-03-204159

Spring 无法解决的情况

情况原因
构造器注入的循环依赖构造函数执行时对象还不存在,无法放入缓存
prototype 作用域的循环依赖prototype 不使用缓存
@Async 等代理 Bean 的某些循环依赖代理对象的创建时机问题

构造器注入为什么无法解决?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class ServiceA {

private final ServiceB serviceB;

public ServiceA(ServiceB serviceB) { // 构造器注入
this.serviceB = serviceB;
}
}

@Service
public class ServiceB {

private final ServiceA serviceA;

public ServiceB(ServiceA serviceA) { // 构造器注入
this.serviceA = serviceA;
}
}

问题在于:

  1. 要创建 ServiceA,必须先有 ServiceB(构造函数参数)
  2. 要创建 ServiceB,必须先有 ServiceA(构造函数参数)
  3. 死锁!谁都创建不出来

4.3.3. 为什么 Spring Boot 2.6+ 默认禁止循环依赖

Spring 团队在 Spring Boot 2.6 中做出了一个重要决定:默认禁止循环依赖。这不是技术限制,而是 设计理念的转变

原因一:循环依赖是设计问题的信号

循环依赖通常意味着:

  • 类的职责划分不清晰
  • 模块之间的边界模糊
  • 代码的耦合度过高

原因二:隐式解决循环依赖会掩盖问题

如果 Spring 默默地解决了循环依赖,开发者可能意识不到代码存在设计问题,直到问题积累到难以维护的程度。

原因三:某些循环依赖会导致诡异的 Bug

特别是涉及 AOP 代理时,循环依赖可能导致:

  • 注入的是原始对象而不是代理对象
  • 事务、缓存等功能失效
  • 难以排查的运行时错误

如果确实需要允许循环依赖(不推荐)

1
2
3
4
# application.yml
spring:
main:
allow-circular-references: true

但这应该是 最后的手段,正确的做法是重构代码消除循环依赖。


4.3.4. 循环依赖的根本解决:重构你的代码

循环依赖是代码在 “求救”——它告诉你这里的设计需要重新思考。以下是四种消除循环依赖的重构策略,理解它们的 核心思想 比记住代码更重要。

策略一:提取公共依赖到第三个类

核心思想

当 A 和 B 互相依赖时,往往是因为它们存在 “职责纠缠”。通过将纠缠的部分提取到独立的类中,可以打破循环。

重构前

image-20260103210633214

问题分析:

  • OrderService 调用 UserService 获取用户信息、给用户加积分
  • UserService 调用 OrderService 查询用户的订单列表
  • 两边都在做一些 “越界” 的事情

重构后

image-20260103210646173

重构要点:

  • 提取纯查询服务UserQueryServiceOrderQueryService 只做数据查询,不依赖其他业务服务
  • 提取独立功能:积分逻辑提取到 PointService
  • 高层服务依赖底层服务:所有箭头单向,无循环

适用场景:两个服务互相调用对方的 “查询” 方法,或职责边界模糊


策略二:使用事件驱动解耦

核心思想

有些循环依赖的本质是 “通知” 关系:A 完成操作后需要触发 B 的动作,但不需要 B 的返回值。这种情况用 事件 替代直接调用,A 和 B 之间就不再有依赖。

重构前

1
2
3
4
5
6
7
8
9
10
11
// OrderService 直接依赖多个后续处理服务
public Order createOrder(Long userId) {
Order order = doCreateOrder(userId);

pointService.addPoints(userId, 100); // 依赖 1
notificationService.notify(userId, order); // 依赖 2
statisticsService.update(userId); // 依赖 3
// 每增加一个后续处理,就多一个依赖...

return order;
}

重构后

1
2
3
4
5
6
7
8
9
10
11
12
// OrderService 只发布事件,不依赖任何后续处理服务
public Order createOrder(Long userId) {
Order order = doCreateOrder(userId);
eventPublisher.publishEvent(new OrderCreatedEvent(order)); // 发布事件
return order;
}

// 各服务独立监听事件
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 加积分、发通知、更新统计...各自处理
}

mermaid-diagram-2026-01-03-210746

适用场景:A 完成后需要 “通知” 多个服务,且不需要它们的返回值


策略三:使用接口隔离

核心思想

A 和 B 互相依赖,但实际上它们只需要对方的 部分功能。通过定义接口,让每个类只依赖它真正需要的能力,而不是整个类。

重构前

1
2
// PaymentService 依赖整个 OrderService,但只用了 updateStatus 方法
// OrderService 依赖整个 PaymentService,但只用了 processPayment 方法

重构后

1
2
3
4
5
6
7
8
9
10
11
// 定义能力接口
interface OrderStatusUpdater {
void updateStatus(Long orderId, String status);
}

interface PaymentProcessor {
boolean processPayment(Long orderId, BigDecimal amount);
}

// OrderService 实现 OrderStatusUpdater,依赖 PaymentProcessor
// PaymentService 实现 PaymentProcessor,依赖 OrderStatusUpdater

image-20260103210908990

关键点:依赖指向接口,而非具体实现。虽然 OrderService 实现了 OrderStatusUpdater,但 PaymentService 依赖的是接口,不是 OrderService 类本身。

适用场景:两个类互相依赖对方的部分功能


策略四:使用 @Lazy(临时方案)

核心思想

@Lazy 让 Spring 注入代理对象而非真实对象,延迟真正 Bean 的创建,从而打破循环。

1
2
3
4
5
6
7
@Service
public class ServiceA {

public ServiceA(@Lazy ServiceB serviceB) { // 注入代理
this.serviceB = serviceB;
}
}

为什么只是临时方案?

  • 只是 绕过 问题,没有 解决 问题
  • 循环依赖说明设计有问题,@Lazy 掩盖了这个信号
  • 应该尽快重构,并在代码中标记 TODO

四种策略对比

策略核心思想适用场景推荐度
提取公共依赖职责分离职责纠缠、边界不清⭐⭐⭐⭐⭐
事件驱动通知解耦A 完成后触发 B,不需要返回值⭐⭐⭐⭐
接口隔离依赖抽象只需要对方的部分功能⭐⭐⭐⭐
@Lazy延迟注入紧急情况的临时方案⭐⭐

最后的建议

遇到循环依赖时,先问自己:

  1. 这两个类为什么要互相依赖?
  2. 它们的职责划分合理吗?
  3. 是否有逻辑放错了地方?

回答这些问题,往往能让你的代码设计更上一层楼。


4.4. 本章总结与依赖问题速查

摘要回顾

本章我们解决了依赖注入中的四类常见问题:多 Bean 选择、可选依赖、循环依赖、以及依赖顺序控制。这些都是实际开发中的高频问题,掌握它们能让你在复杂的依赖场景中游刃有余。


核心概念速查表

问题场景解决方案关键注解/API
多个同类型 Bean,需要默认的标记首选@Primary
多个同类型 Bean,需要精确选择按名称指定@Qualifier("beanName")
多个同类型 Bean,需要全部获取集合注入List<T> / Map<String, T>
依赖可能不存在可选注入Optional<T> / ObjectProvider<T>
根据条件决定是否创建 Bean条件注解@ConditionalOnBean / @ConditionalOnProperty
循环依赖重构代码消除提取公共类 / 事件驱动 / 接口隔离
控制同类型 Bean 的顺序指定顺序值@Order
延迟 Bean 创建懒加载@Lazy