Note 04. 依赖关系的疑难杂症:常见问题与解决方案 摘要 :在前面的章节中,我们学习了如何注册 Bean、如何注入依赖、Bean 的生命周期是怎样的。但在实际开发中,依赖关系往往没有那么简单——一个接口有多个实现类该注入哪个?某个依赖可能不存在怎么办?两个 Bean 互相依赖怎么处理?如何控制 Bean 的创建顺序?本章将逐一解答这些 “疑难杂症”,帮助你在复杂的依赖场景中游刃有余。
本章学习路径
多 Bean 选择 :掌握 @Primary、@Qualifier 等机制,解决 “一个接口多个实现” 的选择问题。可选依赖 :学会处理 “依赖可能不存在” 的场景,避免启动失败。循环依赖 :理解循环依赖的本质,知道如何识别和避免它。顺序控制 :掌握 @DependsOn、@Order 等注解,控制 Bean 的创建和执行顺序。延迟加载 :理解 @Lazy 的原理和适用场景。4.1. 依赖注入中的 “选择困难症”:多个同类型 Bean 在前面的章节中,我们学习的依赖注入场景都比较简单:一个类型对应一个 Bean,Spring 自动找到并注入。但在实际项目中,情况往往没这么单纯——一个接口可能有多个实现类,每个实现类都是一个 Bean 。这时候,Spring 就会陷入 “选择困难症”:你要的是哪一个?
4.1.1. 问题场景:一个接口多个实现 为什么会出现多个同类型的 Bean?
在面向接口编程的实践中,我们经常会定义一个接口,然后提供多个实现。这是一种良好的设计模式,它带来了灵活性和可扩展性。
举个例子:假设我们正在开发一个消息通知系统。通知可以通过多种渠道发送——邮件、短信、App 推送。按照面向接口编程的原则,我们会这样设计:
这个设计非常合理:
统一抽象 :所有发送器都实现同一个接口,调用方不需要关心具体实现易于扩展 :未来要加微信通知?再写一个 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 { 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) { 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) { 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 PushSender implements MessageSender { @Override public void send (String to, String content) { 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 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 public class SmsSender implements MessageSender { } @Component 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; 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 名称 EmailSenderemailSenderSmsSendersmsSenderPushSenderpushSenderUserServiceuserServiceHTTPClientHTTPClient(注意:连续大写字母保持原样)OAuth2ServiceOAuth2Service(注意:以大写字母开头的缩写保持原样)
你也可以在 @Component 中显式指定名称:
1 2 3 4 @Component("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") private MessageSender messageSender; public void notifyUser (String userId, String message) { messageSender.send(userId, message); } }
在构造器注入中使用 @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; 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") 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 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; 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; public DynamicNotificationService (Map<String, MessageSender> senderMap) { this .senderMap = senderMap; System.out.println("可用的发送渠道: " + senderMap.keySet()); } 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) 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):依赖可能不存在 默认情况下,@Autowired 的 required 属性是 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; 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 = "这是报表内容..." ; emailSender.ifPresentOrElse( sender -> sender.send("admin@example.com" , report), () -> System.out.println("邮件发送器未配置,跳过邮件通知" ) ); } }
Optional 的优势
语义清晰 :从类型上就能看出这是一个可选依赖API 丰富 :ifPresent、orElse、map 等方法让处理更优雅避免 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 = "这是报表内容..." ; EmailSender sender = emailSenderProvider.getIfAvailable(); if (sender != null ) { sender.send("admin@example.com" , report); } 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
对比维度 Optional ObjectProvider 来源 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) 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 { @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 (); } @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; public void doSomething () { serviceB.doSomethingElse(); } }
1 2 3 4 5 6 7 8 9 10 @Service public class ServiceB { @Autowired private ServiceA serviceA; public void doSomethingElse () { serviceA.doSomething(); } }
这就形成了一个循环:A → B → A → B → …
更复杂的循环依赖
循环依赖不一定是两个类之间的直接循环,也可能是多个类形成的环:
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 使用 “三级缓存” 来解决这个问题。简单来说:
Spring 开始创建 ServiceA ServiceA 的构造函数执行完毕,得到一个 “半成品” 对象(还没注入依赖) Spring 把这个 “半成品” 放入缓存 Spring 发现 ServiceA 需要 ServiceB,开始创建 ServiceB ServiceB 的构造函数执行完毕,得到 “半成品” Spring 发现 ServiceB 需要 ServiceA,从缓存中取出 ServiceA 的 “半成品” 注入 ServiceB 创建完成 回到 ServiceA,把 ServiceB 注入进去 ServiceA 创建完成
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; } }
问题在于:
要创建 ServiceA,必须先有 ServiceB(构造函数参数) 要创建 ServiceB,必须先有 ServiceA(构造函数参数) 死锁!谁都创建不出来 4.3.3. 为什么 Spring Boot 2.6+ 默认禁止循环依赖 Spring 团队在 Spring Boot 2.6 中做出了一个重要决定:默认禁止循环依赖 。这不是技术限制,而是 设计理念的转变 。
原因一:循环依赖是设计问题的信号
循环依赖通常意味着:
类的职责划分不清晰 模块之间的边界模糊 代码的耦合度过高 原因二:隐式解决循环依赖会掩盖问题
如果 Spring 默默地解决了循环依赖,开发者可能意识不到代码存在设计问题,直到问题积累到难以维护的程度。
原因三:某些循环依赖会导致诡异的 Bug
特别是涉及 AOP 代理时,循环依赖可能导致:
注入的是原始对象而不是代理对象 事务、缓存等功能失效 难以排查的运行时错误 如果确实需要允许循环依赖(不推荐)
1 2 3 4 spring: main: allow-circular-references: true
但这应该是 最后的手段 ,正确的做法是重构代码消除循环依赖。
4.3.4. 循环依赖的根本解决:重构你的代码 循环依赖是代码在 “求救”——它告诉你这里的设计需要重新思考。以下是四种消除循环依赖的重构策略,理解它们的 核心思想 比记住代码更重要。
策略一:提取公共依赖到第三个类 核心思想
当 A 和 B 互相依赖时,往往是因为它们存在 “职责纠缠”。通过将纠缠的部分提取到独立的类中,可以打破循环。
重构前
问题分析:
OrderService 调用 UserService 获取用户信息、给用户加积分 UserService 调用 OrderService 查询用户的订单列表 两边都在做一些 “越界” 的事情 重构后
重构要点:
提取纯查询服务 :UserQueryService、OrderQueryService 只做数据查询,不依赖其他业务服务提取独立功能 :积分逻辑提取到 PointService高层服务依赖底层服务 :所有箭头单向,无循环适用场景 :两个服务互相调用对方的 “查询” 方法,或职责边界模糊
策略二:使用事件驱动解耦 核心思想
有些循环依赖的本质是 “通知” 关系:A 完成操作后需要触发 B 的动作,但不需要 B 的返回值。这种情况用 事件 替代直接调用,A 和 B 之间就不再有依赖。
重构前
1 2 3 4 5 6 7 8 9 10 11 public Order createOrder (Long userId) { Order order = doCreateOrder(userId); pointService.addPoints(userId, 100 ); notificationService.notify(userId, order); statisticsService.update(userId); return order; }
重构后
1 2 3 4 5 6 7 8 9 10 11 12 public Order createOrder (Long userId) { Order order = doCreateOrder(userId); eventPublisher.publishEvent(new OrderCreatedEvent (order)); return order; } @EventListener public void onOrderCreated (OrderCreatedEvent event) { }
适用场景 :A 完成后需要 “通知” 多个服务,且不需要它们的返回值
策略三:使用接口隔离 核心思想
A 和 B 互相依赖,但实际上它们只需要对方的 部分功能 。通过定义接口,让每个类只依赖它真正需要的能力,而不是整个类。
重构前
重构后
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,但 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 延迟注入 紧急情况的临时方案 ⭐⭐
最后的建议
遇到循环依赖时,先问自己:
这两个类为什么要互相依赖? 它们的职责划分合理吗? 是否有逻辑放错了地方? 回答这些问题,往往能让你的代码设计更上一层楼。
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