乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@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 后,会执行以下筛选逻辑:

  1. 检查候选集合中是否有被 @Primary 标记的 Bean
  2. 如果有且仅有一个 @Primary Bean,则注入该 Bean
  3. 如果有多个 @Primary Bean,则抛出 NoUniqueBeanDefinitionException(因为存在多个"默认答案"本身也是歧义)
  4. 如果没有 @Primary Bean,则尝试按字段名/参数名匹配 Bean 名称
  5. 如果仍无法确定,最终抛出 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 中,如果用户自定义了 DataSource Bean,Spring Boot 会在其内置的默认 DataSource 上标注 @Primary,确保用户自定义的优先。反之,如果用户未自定义,则内置的成为唯一候选。这种设计让用户配置天然覆盖框架默认值。

上一页
@Qualifier 详解
下一页
@Resource 详解