Appearance
@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
注解通常用于以下场景:
- 多个相同类型的实现类
- 配置类中的多个 Bean 定义
- 解决自动装配的歧义性
实际业务场景示例
场景一:数据访问层的多种实现
假设我们有一个电商系统,需要支持多种数据存储方式:
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 在进行依赖注入时的选择策略:
- 唯一 Bean:如果只有一个匹配的 Bean,直接注入
- 多个 Bean + @Primary:优先选择标记了
@Primary
的 Bean - 多个 Bean + @Qualifier:根据
@Qualifier
指定的名称选择 - 多个 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
冲突 - ⚠️ 文档化:说明选择某个实现作为主要实现的原因