@Primary 详解
定义与作用
@Primary 是 Spring 提供的首选 Bean(Primary Bean)标记注解,用于解决同一类型存在多个 Bean 时的默认选择问题。当容器中存在多个同类型的候选 Bean,且消费端没有使用 @Qualifier 精确指定时,被标记为 @Primary 的 Bean 会被优先注入。
可以把 @Primary 理解为"默认答案":如果开发者没有特别说明要哪一个,Spring 就注入被 @Primary 标记的那个。它与 @Qualifier 形成互补关系——@Primary 在定义端设定全局默认值,@Qualifier 在消费端进行局部精确覆盖。
在飞翔科技的学生管理系统中,小崔同时维护着 JdbcStudentDao(传统 JDBC 实现)和 JpaStudentDao(JPA 实现)。团队决定逐步迁移到 JPA,但部分旧模块仍依赖 JDBC。白歌建议在 JpaStudentDao 上标注 @Primary,让新开发的业务默认使用 JPA 实现,而旧模块可以通过 @Qualifier("jdbcStudentDao") 显式指定原实现。
适用位置与常用属性
@Primary 没有额外属性,它是一个标记注解(Marker Annotation)。
适用位置:
- 类级别(与
@Component、@Service、@Repository等一起使用) - 方法级别(与
@Bean一起使用)
核心原理
在 Spring 的依赖解析流程中,当 AutowiredAnnotationBeanPostProcessor 按类型找到多个候选 Bean 后,会执行以下筛选逻辑:
- 检查候选集合中是否有被
@Primary标记的 Bean - 如果有且仅有一个
@PrimaryBean,则注入该 Bean - 如果有多个
@PrimaryBean,则抛出NoUniqueBeanDefinitionException(因为存在多个"默认答案"本身也是歧义) - 如果没有
@PrimaryBean,则尝试按字段名/参数名匹配 Bean 名称 - 如果仍无法确定,最终抛出
NoUniqueBeanDefinitionException
完整示例
场景简述
飞翔科技的学生管理系统中,StudentDao 接口有两个实现:JdbcStudentDao(稳定运行两年)和 JpaStudentDao(新开发的现代化实现)。CTO 大翔要求:新模块默认使用 JPA,但旧模块在迁移前保持不动。白歌提出用 @Primary 实现这一策略。
操作前:无 @Primary 导致的启动失败
public interface StudentDao {
Student findById(Long id);
List<Student> findAll();
}
@Repository
public class JdbcStudentDao implements StudentDao {
// JDBC 实现
}
@Repository
public class JpaStudentDao implements StudentDao {
// JPA 实现
}
@Service
public class StudentService {
@Autowired
private StudentDao studentDao; // 报错:2 个候选,无法决定
}
启动报错:
NoUniqueBeanDefinitionException:
expected single matching bean but found 2: jdbcStudentDao, jpaStudentDao
使用该注解的完整代码
package com.feixiang.student.dao;
import com.feixiang.student.entity.Student;
import java.util.List;
public interface StudentDao {
Student findById(Long id);
List<Student> findAll();
}
package com.feixiang.student.dao;
import com.feixiang.student.entity.Student;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class JdbcStudentDao implements StudentDao {
@Override
public Student findById(Long id) {
// 模拟 JDBC 查询
return new Student(id, "小崔-JDBC", 20, "计算机科学");
}
@Override
public List<Student> findAll() {
List<Student> list = new ArrayList<>();
list.add(new Student(1L, "小崔", 20, "计算机科学"));
list.add(new Student(2L, "黄俪", 21, "软件工程"));
return list;
}
}
package com.feixiang.student.dao;
import com.feixiang.student.entity.Student;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
@Primary
public class JpaStudentDao implements StudentDao {
@Override
public Student findById(Long id) {
// 模拟 JPA 查询
return new Student(id, "小崔-JPA", 20, "计算机科学");
}
@Override
public List<Student> findAll() {
List<Student> list = new ArrayList<>();
list.add(new Student(1L, "小崔", 20, "计算机科学"));
list.add(new Student(2L, "黄俪", 21, "软件工程"));
list.add(new Student(3L, "李眉", 22, "网络工程"));
return list;
}
}
package com.feixiang.student.service;
import com.feixiang.student.dao.StudentDao;
import com.feixiang.student.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentService {
private final StudentDao studentDao;
// 未使用 @Qualifier,Spring 自动选择被 @Primary 标记的 JpaStudentDao
@Autowired
public StudentService(StudentDao studentDao) {
this.studentDao = studentDao;
}
public List<Student> listStudents() {
return studentDao.findAll();
}
}
package com.feixiang.student.legacy;
import com.feixiang.student.dao.StudentDao;
import com.feixiang.student.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
// 旧模块显式指定 JDBC 实现,不受 @Primary 影响
@Service
public class LegacyReportService {
private final StudentDao studentDao;
@Autowired
public LegacyReportService(@Qualifier("jdbcStudentDao") StudentDao studentDao) {
this.studentDao = studentDao;
}
public Student getStudent(Long id) {
return studentDao.findById(id);
}
}
package com.feixiang.student;
import com.feixiang.student.legacy.LegacyReportService;
import com.feixiang.student.service.StudentService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class StudentApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext("com.feixiang.student");
StudentService studentService = ctx.getBean(StudentService.class);
LegacyReportService legacyService = ctx.getBean(LegacyReportService.class);
System.out.println("新模块 StudentService 查询结果:" + studentService.listStudents().size() + " 条");
System.out.println("旧模块 LegacyReportService 查询结果:" + legacyService.getStudent(1L).getName());
ctx.close();
}
}
操作后运行结果及分析
新模块 StudentService 查询结果:3 条
旧模块 LegacyReportService 查询结果:小崔-JDBC
变化分析:
StudentService没有使用@Qualifier,Spring 自动注入被@Primary标记的JpaStudentDao,返回 3 条数据(JPA 实现包含更多数据源)LegacyReportService显式使用@Qualifier("jdbcStudentDao"),不受@Primary影响,仍然使用 JDBC 实现- 团队可以逐步将旧模块从
@Qualifier("jdbcStudentDao")移除,平滑迁移到 JPA,无需一次性全量改造
@Primary 与 @Bean 方法
@Primary 也可以用在 @Bean 方法上,常用于配置第三方库的多个同类实例:
@Configuration
public class CacheConfig {
@Bean
@Primary
public CacheManager redisCacheManager() {
return new RedisCacheManager();
}
@Bean
public CacheManager caffeineCacheManager() {
return new CaffeineCacheManager();
}
}
消费端未标注 @Qualifier 时,默认注入 redisCacheManager;需要本地缓存时显式指定 @Qualifier("caffeineCacheManager")。
易错场景与面试考点
易错场景一:多个 @Primary 导致的新歧义
@Repository
@Primary
public class JpaStudentDao implements StudentDao { }
@Repository
@Primary
public class MongoStudentDao implements StudentDao { }
@Service
public class StudentService {
@Autowired
private StudentDao studentDao; // 报错:2 个 @Primary
}
后果:启动报错 NoUniqueBeanDefinitionException: more than one 'primary' bean found。
正确理解:@Primary 的含义是"当存在歧义时的默认选择",因此同一类型的候选集合中只能有一个 @Primary。如果业务上确实需要多个"主要"实现,说明应该使用 @Qualifier 在消费端明确区分,而非滥用 @Primary。
易错场景二:@Primary 与 @Qualifier 的优先级误解
@Repository
@Primary
public class JpaStudentDao implements StudentDao { }
@Service
public class StudentService {
@Autowired
@Qualifier("jdbcStudentDao")
private StudentDao studentDao;
}
结果:@Qualifier 的优先级高于 @Primary,最终注入的是 jdbcStudentDao。
正确理解:歧义解决的优先级顺序为:@Qualifier(消费端精确指定)> @Primary(定义端全局默认)> 字段名匹配 > 抛出异常。@Qualifier 是局部覆盖,@Primary 是全局默认值。
面试考点
Q:@Primary 和 @Qualifier 如何选择?
如果大多数消费端都应该使用同一个实现,少数需要特殊处理,则在主要实现上标注
@Primary,特殊消费端用@Qualifier覆盖。如果各个实现的使用频率相当,没有明显的"主要"实现,则不应使用@Primary,而应在每个消费端都用@Qualifier明确指定,避免隐式约定带来的维护成本。
Q:@Primary 可以用在 @Bean 的参数上吗?
不可以。
@Primary只能标注在定义端(类或@Bean方法),不能标注在注入点(字段、参数)。注入点若需精确指定,应使用@Qualifier。
Q:@Primary 对 @Resource 生效吗?
不生效。
@Resource默认按名称装配,其解析流程不参考@Primary。只有当@Resource按名称找不到、回退到按类型匹配时,@Primary才会参与类型匹配的歧义解决。但依赖@Resource的回退行为是不稳定的,不应作为设计依据。
Q:Spring Boot 自动配置中 @Primary 的典型应用是什么?
Spring Boot 的
DataSourceAutoConfiguration中,如果用户自定义了DataSourceBean,Spring Boot 会在其内置的默认DataSource上标注@Primary,确保用户自定义的优先。反之,如果用户未自定义,则内置的成为唯一候选。这种设计让用户配置天然覆盖框架默认值。