在 Spring Boot 项目里,面向接口编程是最常见的开发方式。我们定义一个 Service 接口,然后给出一个或多个实现类,交给 Spring 容器管理,使用的时候直接通过类型注入即可。但一旦同一个接口出现了两个甚至更多的实现类,而没有做任何额外声明时,容器启动就会报 NoUniqueBeanDefinitionException,提示期望单个匹配的 Bean 却找到了多个。解决这个问题最直接的手段之一,就是使用 @Primary 注解标记一个优先级最高的实现。本文将从原理、用法、对比和典型场景几个方面,详细讲解 @Primary 的正确使用方式。

为什么会出现多实现 Bean 冲突
Spring 的依赖注入默认是按照类型(byType)进行的。当容器中某个类型的 Bean 只有一个时,注入没有任何歧义。但如果同一个接口注册了多个实现,Spring 就无法判断该把哪一个注入进来。看下面这段典型代码:
public interface MessageService {
void send(String content);
}
@Service
public class EmailService implements MessageService {
@Override
public void send(String content) {
System.out.println("通过邮件发送:" + content);
}
}
@Service
public class SmsService implements MessageService {
@Override
public void send(String content) {
System.out.println("通过短信发送:" + content);
}
}两个实现类都标注了 @Service,都会被组件扫描注册为容器中的 Bean。此时如果在另一个 Bean 中这样注入:
@Service
public class NotifyFacade {
@Autowired
private MessageService messageService; // 启动时报错
}启动时就会抛出类似 expected single matching bean but found 2: emailService, smsService 的异常。原因很简单:字段类型是 MessageService,容器里能匹配到两个候选者,Spring 不知道你想要哪一个。了解这个根本原因之后,解决思路也就清晰了:要么精确指定名字,要么告诉容器一个默认选择。@Primary 走的就是第二条路。
@Primary 的基本用法与底层逻辑
@Primary 的含义是:在同类型的多个候选 Bean 中,被标注的这个享有最高优先级。当注入点没有指定其他限定条件时,Spring 会优先选择标记了 @Primary 的那一个。用法非常简单,直接加在实现类上:
@Service
@Primary
public class EmailService implements MessageService {
@Override
public void send(String content) {
System.out.println("通过邮件发送:" + content);
}
}加了 @Primary 之后,前文的 NotifyFacade 可以正常启动,注入的就是 EmailService。需要注意的是,@Primary 不仅可以用在类上,还可以用在 @Bean 方法上、@Component 修饰的类上,甚至可以配合 @Configuration 中的方法一起使用:
@Configuration
public class MessageConfig {
@Bean
@Primary
public MessageService emailService() {
return new EmailService();
}
@Bean
public MessageService smsService() {
return new SmsService();
}
}从底层看,Spring 在解析依赖时,会先收集所有与目标类型匹配的候选 Bean,然后判断这些候选者中是否存在主候选者(Primary)。如果恰好有一个 Primary,就直接采用;如果有两个以上都标了 @Primary,同样会抛出异常。所以一个类型体系中,@Primary 只能有一个。另外要提醒的是,通过构造器注入、Setter 注入还是字段注入,@Primary 的生效机制是一样的,它作用在候选者筛选阶段,与注入方式无关。
@Primary 与 @Qualifier、@Resource 的选择
解决多实现注入还有另外两个常见方案:@Qualifier 和 @Resource。它们和 @Primary 的定位并不相同,理解差异才能选对工具。
@Qualifier 是精确匹配,写在注入点上,通过 Bean 名称或者自定义限定符明确指出要注入哪一个;@Primary 是默认兜底,写在被注入的 Bean 一方,为整条类型链指定一个默认值。如果两者同时出现,@Qualifier 的优先级更高。看下面这个例子:
@Service
public class NotifyFacade {
@Autowired
@Qualifier("smsService")
private MessageService messageService; // 注入的是 SmsService 而不是 Primary 的 EmailService
}而 @Resource 是 JDK 提供的注解,默认按字段名匹配 Bean 名称。如果按名称找不到,再退回到按类型匹配。实际项目中推荐的组合方式是:给最常用、最通用的实现标 @Primary,其他需要切换的地方用 @Qualifier 指名道姓。这样大多数注入点可以保持简洁,只在有特殊需求时额外声明。此外,Spring 还提供了 @Priority 注解(JSR-250 标准),可以给候选 Bean 排定次序,数值越小优先级越高,但 @Primary 的优先级仍然高于 @Priority。
典型场景与常见坑
第一个典型场景是自定义 Starter。当你开发一个通用组件时,往往希望提供一个默认实现,同时允许业务方覆盖。在默认实现的 @Bean 方法上加上 @ConditionalOnMissingBean 就能做到,但如果是自动装配的默认实现类与业务方实现共存的情况,给默认实现加 @Primary 也是一种常见做法。不过要小心:一旦业务方新写了一个实现却忘了处理冲突,运行行为可能不符合预期,排查起来比较隐蔽。
第二个场景是多数据源配置。配置多个 DataSource 时,通常会给主数据源标 @Primary,这样 JdbcTemplate、事务管理器等依赖 DataSource 的组件在未指定的情况下都能拿到主库连接:
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
}最后总结几个容易踩的坑:一是一个类型只能有一个 @Primary,标多了照样报错;二是 @Primary 只解决注入歧义,不会改变 getBeansOfType 返回多个结果的事实,需要收集所有实现时仍然可以注入 List<MessageService> 或 Map<String, MessageService>;三是如果使用 @Profile 切换环境实现,两个不同 Profile 的实现类同时标 @Primary 是可以的,因为同一时刻只有一个会被激活,但前提是 Profile 控制要严谨,避免两个环境的 Bean 同时生效。掌握这些细节,多实现的注入问题基本都能从容应对。
Spring Boot@Primary依赖注入修改时间:2026-09-12 14:42:35