Skip to content

@Primary 注解详解

概述

@Primary 注解是 Spring Framework 中用于解决依赖注入歧义性的重要注解。当 Spring 容器中存在多个相同类型的 Bean 时,@Primary 注解可以指定哪个 Bean 作为首选的注入候选者。

INFO

核心概念当存在多个候选 Bean 时,@Primary 注解标记的 Bean 将优先被选择进行自动装配。

基本用法

注解定义

kotlin
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Documented
annotation class Primary

使用场景

@Primary 注解通常用于以下场景:

  1. 多个相同类型的实现类
  2. 配置类中的多个 Bean 定义
  3. 解决自动装配的歧义性

实际业务场景示例

场景一:数据访问层的多种实现

假设我们有一个电商系统,需要支持多种数据存储方式:

kotlin
// 抽象的用户仓储接口
interface UserRepository {
    fun findById(id: Long): User?
    fun save(user: User): User
    fun findAll(): List<User>
}

// MySQL 实现
@Repository
class MySqlUserRepository : UserRepository {
    override fun findById(id: Long): User? {
        // MySQL 数据库查询逻辑
        println("从 MySQL 数据库查询用户: $id")
        return User(id, "用户$id", "user$id@example.com")
    }

    override fun save(user: User): User {
        println("保存用户到 MySQL: ${user.name}")
        return user
    }

    override fun findAll(): List<User> {
        println("从 MySQL 获取所有用户")
        return listOf()
    }
}

// Redis 缓存实现
@Primary
@Repository
class RedisUserRepository : UserRepository {
    override fun findById(id: Long): User? {
        // Redis 缓存查询逻辑
        println("从 Redis 缓存查询用户: $id")
        return User(id, "缓存用户$id", "cache$id@example.com")
    }

    override fun save(user: User): User {
        println("保存用户到 Redis 缓存: ${user.name}")
        return user
    }

    override fun findAll(): List<User> {
        println("从 Redis 获取所有用户")
        return listOf()
    }
}

// 用户服务
@Service
class UserService(
    private val userRepository: UserRepository // 会自动注入 RedisUserRepository
) {
    fun getUser(id: Long): User? {
        return userRepository.findById(id)
    }

    fun createUser(name: String, email: String): User {
        val user = User(0, name, email)
        return userRepository.save(user)
    }
}

TIP

为什么选择 Redis 作为主要实现?在这个例子中,我们将 RedisUserRepository 标记为 @Primary,因为:

  • 缓存访问速度更快
  • 减少数据库压力
  • 提供更好的用户体验

场景二:配置类中的多个 Bean 定义

kotlin
@Configuration
class DataSourceConfiguration {

    @Bean("primaryDataSource")
    @Primary
    @ConfigurationProperties("app.datasource.primary")
    fun primaryDataSource(): DataSource {
        return HikariDataSource().apply {
            jdbcUrl = "jdbc:mysql://localhost:3306/primary_db"
            username = "primary_user"
            password = "primary_pass"
            maximumPoolSize = 20
        }
    }

    @Bean("secondaryDataSource")
    @ConfigurationProperties("app.datasource.secondary")
    fun secondaryDataSource(): DataSource {
        return HikariDataSource().apply {
            jdbcUrl = "jdbc:mysql://localhost:3306/secondary_db"
            username = "secondary_user"
            password = "secondary_pass"
            maximumPoolSize = 10
        }
    }
}

@Service
class OrderService(
    private val dataSource: DataSource // 自动注入 primaryDataSource
) {
    fun processOrder(order: Order) {
        // 使用主数据源处理订单
        dataSource.connection.use { conn ->
            // 订单处理逻辑
            println("使用主数据源处理订单: ${order.id}")
        }
    }
}

工作原理

依赖注入流程图

Bean 选择策略

Spring 在进行依赖注入时的选择策略:

  1. 唯一 Bean:如果只有一个匹配的 Bean,直接注入
  2. 多个 Bean + @Primary:优先选择标记了 @Primary 的 Bean
  3. 多个 Bean + @Qualifier:根据 @Qualifier 指定的名称选择
  4. 多个 Bean + 无标识:抛出 NoUniqueBeanDefinitionException

高级特性

与其他注解的组合使用

kotlin
@Configuration
class MessageConfiguration {

    @Bean
    @Primary
    @ConditionalOnProperty(name = "messaging.type", havingValue = "kafka")
    fun kafkaMessageSender(): MessageSender {
        return KafkaMessageSender()
    }

    @Bean
    @ConditionalOnProperty(name = "messaging.type", havingValue = "rabbitmq")
    fun rabbitMessageSender(): MessageSender {
        return RabbitMessageSender()
    }

    @Bean
    @ConditionalOnMissingBean
    fun defaultMessageSender(): MessageSender {
        return DefaultMessageSender()
    }
}

集合注入的特殊行为

> `@Primary` 注解只对单一值注入有效,对集合、数组、Map 等多值注入无效。

kotlin
@Service
class NotificationService(
    private val primarySender: MessageSender, // 会注入 @Primary 标记的 Bean
    private val allSenders: List<MessageSender> // 会注入所有 MessageSender 类型的 Bean
) {

    fun sendImportantMessage(message: String) {
        // 使用主要的发送器发送重要消息
        primarySender.send(message)
    }

    fun broadcastMessage(message: String) {
        // 使用所有可用的发送器广播消息
        allSenders.forEach { sender ->
            sender.send(message)
        }
    }
}

最佳实践

1. 明确的命名策略

kotlin
@Primary
@Service("primaryUserService")
class DefaultUserService : UserService {
    // 默认实现
}

@Service("adminUserService")
class AdminUserService : UserService {
    // 管理员专用实现
}

2. 配合 Profile 使用

kotlin
@Profile("production")
@Primary
@Repository
class ProductionUserRepository : UserRepository {
    // 生产环境实现
}

@Profile("development")
@Repository
class DevelopmentUserRepository : UserRepository {
    // 开发环境实现
}

3. 文档化决策

kotlin
/**
 * Redis 用户仓储实现
 *
 * 标记为 @Primary 是因为:
 * 1. 提供更快的查询性能
 * 2. 减少数据库负载
 * 3. 支持分布式缓存
 */
@Primary
@Repository
class RedisUserRepository : UserRepository {
    // 实现细节
}
kotlin
@Primary // 为什么是主要的?没有说明
@Repository
class SomeUserRepository : UserRepository {
    // 实现细节
}

测试验证

单元测试示例

kotlin
@SpringBootTest
class PrimaryAnnotationTest {

    @Autowired
    private lateinit var userRepository: UserRepository

    @Test
    fun `should inject primary repository`() {
        // 验证注入的是 RedisUserRepository
        assertThat(userRepository).isInstanceOf(RedisUserRepository::class.java)
    }

    @Test
    fun `should use primary repository in service`() {
        val user = userRepository.findById(1L)
        // 验证调用的是 Redis 实现
        assertThat(user?.email).contains("cache")
    }
}

集成测试

kotlin
@TestConfiguration
class TestPrimaryConfiguration {

    @Primary
    @Bean
    fun testUserRepository(): UserRepository {
        return mockk<UserRepository>()
    }
}

@SpringBootTest
@Import(TestPrimaryConfiguration::class)
class PrimaryIntegrationTest {

    @Autowired
    private lateinit var userService: UserService

    @MockkBean
    @Qualifier("testUserRepository")
    private lateinit var mockRepository: UserRepository

    @Test
    fun `should use test primary repository`() {
        every { mockRepository.findById(any()) } returns User(1, "测试用户", "[email protected]")

        val user = userService.getUser(1L)

        assertThat(user?.name).isEqualTo("测试用户")
        verify { mockRepository.findById(1L) }
    }
}

总结

@Primary 注解是 Spring 依赖注入中解决歧义性的重要工具:

  • 简化配置:无需复杂的 @Qualifier 配置
  • 提高可读性:明确标识主要实现
  • 灵活切换:易于在不同实现间切换
  • ⚠️ 谨慎使用:避免多个 @Primary 冲突
  • ⚠️ 文档化:说明选择某个实现作为主要实现的原因