Kotlin 注解
注解(Annotation)是一种将元数据附加到代码中的机制。通过注解,我们可以为类、函数、属性等元素添加额外信息,这些信息可以在编译时或运行时被工具和框架读取和使用。本章将详细介绍如何声明和使用注解。
注解基础概念
什么是注解?
注解是一种特殊的标记,可以被添加到代码的各个元素上(类、函数、属性、参数等)。注解本身不执行任何代码,但它们可以被编译器、注解处理器或运行时反射读取,从而实现特定的功能。
// 常见的内置注解示例
@Deprecated("使用 newFunction() 替代", ReplaceWith("newFunction()"))
fun oldFunction() { }
fun newFunction() { }
注解的作用
- 编译时检查:如
@Deprecated警告、@Suppress抑制警告 - 代码生成:通过注解处理器自动生成代码
- 运行时行为:框架通过反射读取注解来改变程序行为
- 文档生成:生成 API 文档时的额外信息
声明注解
基本语法
使用 annotation class 关键字声明注解:
// 最简单的注解
annotation class Review
// 使用注解
@Review
class CodeDocument {
@Review
fun process() { }
}
带参数的注解
注解可以有构造函数参数:
// 带参数的注解
annotation class Author(val name: String)
// 使用
@Author("张三")
class MyClass {
@Author("李四")
fun method() { }
}
// 多个参数
annotation class Api(
val version: String,
val deprecated: Boolean = false,
val description: String = ""
)
@Api(version = "2.0", description = "用户接口")
interface UserService {
@Api(version = "1.0", deprecated = true)
fun oldMethod()
@Api(version = "2.0")
fun newMethod()
}
注解参数的限制
注解参数的类型有限制,只能使用以下类型:
annotation class ValidTypes(
// 基本类型
val int: Int,
val long: Long,
val double: Double,
val float: Float,
val boolean: Boolean,
val byte: Byte,
val short: Short,
val char: Char,
// 字符串
val string: String,
// 枚举
val enum: Thread.State,
// 类引用
val klass: KClass<*>,
// 数组(元素类型也受限制)
val intArray: IntArray,
val stringArray: Array<String>,
// 其他注解
val anotherAnnotation: Author
)
// 使用示例
@ValidTypes(
int = 1,
long = 2L,
double = 3.0,
float = 4.0f,
boolean = true,
byte = 5,
short = 6,
char = 'A',
string = "hello",
enum = Thread.State.NEW,
klass = String::class,
intArray = [1, 2, 3],
stringArray = ["a", "b"],
anotherAnnotation = Author("张三")
)
class Example
注意:注解参数不能为可空类型(String? 不允许),因为 JVM 不支持将 null 存储为注解属性值。
// 错误:参数不能可空
// annotation class Bad(val name: String?)
元注解
元注解是用于注解注解本身的注解,用于控制注解的行为。
@Target - 指定目标
@Target 指定注解可以应用的目标元素:
// 只能用于类
@Target(AnnotationTarget.CLASS)
annotation class Entity
@Entity // 正确
class User
// @Entity fun wrong() = Unit // 编译错误
// 可以用于多个目标
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY
)
annotation class Documented
@Documented
class Service {
@Documented
val name: String = ""
@Documented
fun execute() { }
}
可用的目标类型:
| 目标类型 | 说明 |
|---|---|
CLASS | 类、接口、对象 |
ANNOTATION_CLASS | 注解类 |
PROPERTY | 属性(Kotlin 特有) |
FIELD | 字段(Java 字段) |
LOCAL_VARIABLE | 局部变量 |
VALUE_PARAMETER | 函数参数值 |
CONSTRUCTOR | 构造函数 |
FUNCTION | 函数 |
PROPERTY_GETTER | 属性 getter |
PROPERTY_SETTER | 属性 setter |
TYPE | 类型(泛型参数、返回类型等) |
EXPRESSION | 表达式 |
FILE | 文件 |
TYPEALIAS | 类型别名 |
TYPE_PARAMETER | 类型参数 |
@Retention - 指定保留策略
@Retention 指定注解在什么阶段保留:
// SOURCE:仅在源代码保留,编译后丢弃
@Retention(AnnotationRetention.SOURCE)
annotation class SourceOnly
// BINARY:编译到 class 文件,但运行时不可见(默认)
@Retention(AnnotationRetention.BINARY)
annotation class BinaryRetention
// RUNTIME:编译到 class 文件,运行时可通过反射访问
@Retention(AnnotationRetention.RUNTIME)
annotation class RuntimeRetention
保留策略对比:
| 策略 | 源代码 | 字节码 | 运行时反射 | 使用场景 |
|---|---|---|---|---|
SOURCE | ✓ | ✗ | ✗ | 编译时检查、代码生成 |
BINARY | ✓ | ✓ | ✗ | 编译时处理、工具分析 |
RUNTIME | ✓ | ✓ | ✓ | 运行时框架、反射处理 |
@Repeatable - 可重复注解
@Repeatable 允许同一注解多次应用于同一元素:
@Repeatable
annotation class Tag(val name: String)
// 可以重复使用
@Tag("important")
@Tag("urgent")
@Tag("review")
class Task
// 编译器自动生成容器注解 Tag.Container
自定义容器注解:
@JvmRepeatable(Tags::class)
annotation class Label(val value: String)
// 自定义容器
annotation class Tags(val values: Array<Label>)
@Label("backend")
@Label("api")
@Label("v2")
class ApiController
@MustBeDocumented - 文档包含
@MustBeDocumented 指定注解应包含在生成的 API 文档中:
@MustBeDocumented
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Since(val version: String)
@Since("2.0")
class NewApi {
@Since("2.1")
fun latestFeature() { }
}
注解使用目标
当注解 Kotlin 代码时,有时需要明确指定注解应用于哪个 Java 元素。
使用目标语法
class Example(
@field:Ann val foo, // 注解字段
@get:Ann val bar, // 注解 getter
@param:Ann val quux // 注解构造函数参数
)
annotation class Ann
可用的使用目标
| 目标 | 说明 |
|---|---|
file | 文件 |
property | Kotlin 属性(Kotlin 不会生成注解) |
field | Java 字段 |
get | 属性 getter |
set | 属性 setter |
receiver | 扩展函数/属性的接收者 |
param | 构造函数参数 |
setparam | setter 参数 |
delegate | 委托属性存储委托实例的字段 |
实际应用示例
// 文件级注解
@file:JvmName("StringUtils")
package com.example
// Java 互操作示例
import javax.inject.Inject
class UserPresenter {
// 注解字段
@field:Inject
lateinit var repository: UserRepository
// 注解 getter
@get:Synchronized
val count: Int = 0
// 注解 setter 参数
var name: String = ""
set(@setparam:NotEmpty value) {
field = value
}
}
// 注解扩展函数接收者
annotation class ReceiverAnn
fun @ReceiverAnn String.myExtension(): Int = this.length
annotation class NotEmpty
annotation class Synchronized
class UserRepository
默认目标选择
如果未指定使用目标,按以下顺序选择:
param(构造函数参数)property(Kotlin 1.1+)field
内置注解
@Deprecated
标记已废弃的元素:
// 基本用法
@Deprecated("此方法已废弃")
fun oldMethod() { }
// 完整用法
@Deprecated(
message = "使用 newMethod() 替代",
replaceWith = ReplaceWith("newMethod()"),
level = DeprecationLevel.ERROR
)
fun deprecatedMethod() { }
fun newMethod() { }
fun main() {
// oldMethod() // 警告
// deprecatedMethod() // 编译错误
newMethod() // 正常
}
DeprecationLevel 级别:
WARNING:编译警告(默认)ERROR:编译错误HIDDEN:代码中不可见
@Suppress
抑制编译器警告:
// 抑制特定警告
@Suppress("UNCHECKED_CAST")
fun castExample() {
val list: List<Any> = listOf(1, 2, 3)
val intList = list as List<Int> // 未检查的转换
}
// 抑制多个警告
@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
fun unusedExample(a: Int, b: String) {
val c = 100 // 未使用
}
常用抑制选项:
UNCHECKED_CAST:未检查的类型转换UNUSED_PARAMETER:未使用的参数UNUSED_VARIABLE:未使用的变量DEPRECATION:废弃 API 使用NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS:Java 空安全警告
@JvmStatic
为伴生对象方法生成静态方法:
class Utils {
companion object {
@JvmStatic
fun staticMethod() = "静态方法"
fun normalMethod() = "普通方法"
}
}
// Java 调用:
// Utils.staticMethod() // 直接调用
// Utils.Companion.normalMethod() // 需要通过 Companion
@JvmField
暴露属性为公共字段:
class Config {
companion object {
@JvmField
val VERSION = "1.0"
// 普通属性会生成 getter
val NAME = "MyApp"
}
}
// Java 调用:
// Config.VERSION // 直接访问字段
// Config.getNAME() // 通过 getter
@JvmName
指定 Java 中的名称:
@file:JvmName("StringUtils") // 文件类名
// 函数名
@JvmName("addPrefix")
fun String.prefix(p: String) = "$p$this"
// Kotlin 调用: "hello".prefix(">> ")
// Java 调用: StringUtils.addPrefix("hello", ">> ")
@JvmOverloads
生成重载方法:
class Button(
@JvmField val text: String,
val width: Int = 100,
val height: Int = 50
) {
// 生成 4 个构造函数:
// Button(text)
// Button(text, width)
// Button(text, width, height)
}
@JvmOverloads
fun createUser(
name: String,
age: Int = 0,
email: String = ""
): User {
return User(name, age, email)
}
data class User(val name: String, val age: Int, val email: String)
// Java 调用:
// createUser("张三")
// createUser("张三", 25)
// createUser("张三", 25, "[email protected]")
@Throws
声明函数可能抛出的异常(用于 Java 互操作):
@Throws(IOException::class)
fun readFile(path: String): String {
return java.io.File(path).readText()
}
// Java 调用:
// try {
// readFile("test.txt");
// } catch (IOException e) {
// // 处理异常
// }
@Volatile
将属性标记为易变的(线程可见性):
class SharedResource {
@Volatile
var running = true
fun stop() {
running = false
}
}
@Synchronized
同步方法:
class Counter {
private var count = 0
@Synchronized
fun increment(): Int {
return ++count
}
@Synchronized
fun decrement(): Int {
return --count
}
}
注解与反射
使用反射在运行时读取注解需要将注解的 @Retention 设置为 RUNTIME。
读取类注解
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Table(val name: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class Column(val name: String = "", val primaryKey: Boolean = false)
@Table(name = "users")
data class User(
@Column(name = "id", primaryKey = true)
val id: Long,
@Column(name = "username")
val name: String,
@Column("email")
val email: String
)
fun main() {
// 获取类上的注解
val tableAnnotation = User::class.findAnnotation<Table>()
println("表名: ${tableAnnotation?.name}") // 表名: users
// 获取所有属性上的注解
User::class.memberProperties.forEach { prop ->
val columnAnnotation = prop.findAnnotation<Column>()
println("${prop.name} -> ${columnAnnotation?.name}, PK: ${columnAnnotation?.primaryKey}")
}
// id -> id, PK: true
// name -> username, PK: false
// email -> email, PK: false
}
读取函数注解
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Route(val path: String, val method: String = "GET")
class ApiController {
@Route("/users", "GET")
fun getUsers(): List<String> = listOf("user1", "user2")
@Route("/users", "POST")
fun createUser(): String = "created"
@Route("/users/{id}", "DELETE")
fun deleteUser(): String = "deleted"
}
fun main() {
// 获取所有带 @Route 注解的方法
ApiController::class.memberFunctions
.filter { it.findAnnotation<Route>() != null }
.forEach { func ->
val route = func.findAnnotation<Route>()!!
println("${route.method} ${route.path} -> ${func.name}")
}
// GET /users -> getUsers
// POST /users -> createUser
// DELETE /users/{id} -> deleteUser
}
读取参数注解
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Body
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Query(val name: String)
class UserService {
fun search(
@Query("q") query: String,
@Query("page") page: Int,
@Body body: String
) { }
}
fun main() {
val func = UserService::class.memberFunctions.find { it.name == "search" }!!
func.valueParameters.forEach { param ->
val queryAnn = param.findAnnotation<Query>()
val bodyAnn = param.findAnnotation<Body>()
when {
queryAnn != null -> println("Query参数: ${param.name} -> ${queryAnn.name}")
bodyAnn != null -> println("Body参数: ${param.name}")
}
}
// Query参数: query -> q
// Query参数: page -> page
// Body参数: body
}
注解处理器
注解处理器(Annotation Processor)在编译时处理注解,可以生成新代码。虽然详细的注解处理器开发超出本章范围,这里介绍基本概念。
KSP(Kotlin Symbol Processing)
KSP 是 Kotlin 官方推荐的注解处理工具:
// build.gradle.kts
// plugins {
// id("com.google.devtools.ksp") version "1.9.0-1.0.13"
// }
// dependencies {
// ksp("com.example:processor:1.0.0")
// }
使用示例:自动生成 Builder
// 定义注解
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Builder
// 使用注解
@Builder
data class Person(
val name: String,
val age: Int,
val email: String? = null
)
// 注解处理器会自动生成:
// class PersonBuilder {
// var name: String? = null
// var age: Int? = null
// var email: String? = null
//
// fun build(): Person {
// return Person(
// name = name ?: throw IllegalArgumentException(),
// age = age ?: throw IllegalArgumentException(),
// email = email
// )
// }
// }
实战:自定义 ORM 映射
下面是一个使用注解实现简单 ORM 映射的示例:
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.*
// 注解定义
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Entity(val tableName: String = "")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class Id(val autoGenerate: Boolean = true)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
annotation class Column(
val name: String = "",
val nullable: Boolean = true
)
// 实体类
@Entity("t_user")
data class User(
@Id
@Column("user_id")
val id: Long,
@Column("user_name", nullable = false)
val name: String,
@Column("user_email")
val email: String?,
@Column(nullable = false)
val createdAt: String
)
// ORM 映射工具
object OrmMapper {
// 获取表名
fun <T : Any> getTableName(clazz: KClass<T>): String {
val entityAnn = clazz.findAnnotation<Entity>()
return entityAnn?.tableName?.takeIf { it.isNotEmpty() }
?: clazz.simpleName?.lowercase() ?: ""
}
// 获取主键列
fun <T : Any> getPrimaryKey(clazz: KClass<T>): Pair<String, KProperty1<T, *>>? {
for (prop in clazz.memberProperties) {
if (prop.findAnnotation<Id>() != null) {
val columnAnn = prop.findAnnotation<Column>()
val columnName = columnAnn?.name?.takeIf { it.isNotEmpty() }
?: prop.name
return columnName to prop
}
}
return null
}
// 获取所有列映射
fun <T : Any> getColumnMappings(clazz: KClass<T>): Map<String, KProperty1<T, *>> {
return clazz.memberProperties.associate { prop ->
val columnAnn = prop.findAnnotation<Column>()
val columnName = columnAnn?.name?.takeIf { it.isNotEmpty() }
?: prop.name
columnName to prop
}
}
// 生成 SELECT SQL
fun <T : Any> generateSelectSql(clazz: KClass<T>): String {
val tableName = getTableName(clazz)
val columns = getColumnMappings(clazz).keys.joinToString(", ")
return "SELECT $columns FROM $tableName"
}
// 生成 INSERT SQL
fun <T : Any> generateInsertSql(clazz: KClass<T>, entity: T): String {
val tableName = getTableName(clazz)
val mappings = getColumnMappings(clazz)
val columns = mappings.keys.joinToString(", ")
val values = mappings.values.map { prop ->
val value = prop.get(entity)
when (value) {
is String -> "'$value'"
null -> "NULL"
else -> value.toString()
}
}.joinToString(", ")
return "INSERT INTO $tableName ($columns) VALUES ($values)"
}
// 从 ResultSet 映射到实体
fun <T : Any> mapFromResultSet(
clazz: KClass<T>,
data: Map<String, Any?>
): T? {
val constructor = clazz.primaryConstructor ?: return null
val args = constructor.parameters.associateWith { param ->
val prop = clazz.memberProperties.find { it.name == param.name }
val columnAnn = prop?.findAnnotation<Column>()
val columnName = columnAnn?.name?.takeIf { it.isNotEmpty() }
?: param.name ?: ""
data[columnName]
}
return try {
constructor.callBy(args)
} catch (e: Exception) {
null
}
}
}
fun main() {
// 演示 ORM 映射
println("=== ORM 映射示例 ===")
println("表名: ${OrmMapper.getTableName(User::class)}")
println("\n列映射:")
OrmMapper.getColumnMappings(User::class).forEach { (column, prop) ->
val isId = prop.findAnnotation<Id>() != null
println(" $column -> ${prop.name} ${if (isId) "[PK]" else ""}")
}
println("\nSELECT SQL:")
println(" ${OrmMapper.generateSelectSql(User::class)}")
val user = User(1L, "张三", "[email protected]", "2024-01-01")
println("\nINSERT SQL:")
println(" ${OrmMapper.generateInsertSql(User::class, user)}")
// 模拟从数据库读取
val dbData = mapOf(
"user_id" to 2L,
"user_name" to "李四",
"user_email" to "[email protected]",
"createdAt" to "2024-01-02"
)
println("\n映射到实体:")
val mappedUser = OrmMapper.mapFromResultSet(User::class, dbData)
println(" $mappedUser")
}
最佳实践
1. 选择正确的保留策略
// 编译时检查:使用 SOURCE
@Retention(AnnotationRetention.SOURCE)
annotation class CheckPermission
// 框架使用:使用 RUNTIME
@Retention(AnnotationRetention.RUNTIME)
annotation class Controller
2. 提供清晰的文档
/**
* 标记一个类为 REST 控制器。
*
* @param value 控制器的基础路径
* @since 1.0
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class RestController(val value: String = "")
3. 使用合理的默认值
// 提供合理的默认值
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Cache(
val key: String = "",
val ttl: Long = 3600, // 默认 1 小时
val enabled: Boolean = true
)
4. 组合注解
// 定义组合注解
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@JvmRecord
@Serializable
annotation class Dto
// 使用
@Dto
data class UserDto(val id: Long, val name: String)
// 需要导入的注解
annotation class Serializable
annotation class JvmRecord
小结
本章我们学习了 Kotlin 注解的核心内容:
- 注解基础:声明、参数、使用方式
- 元注解:
@Target、@Retention、@Repeatable、@MustBeDocumented - 使用目标:
@file:、@field:、@get:等 - 内置注解:
@Deprecated、@Suppress、@JvmStatic、@JvmField等 - 反射读取:运行时获取注解信息
- 最佳实践:选择保留策略、提供文档、使用默认值
注解是 Kotlin 元编程的重要工具,结合反射可以实现强大的框架功能。理解注解的使用方式对于使用各种 Kotlin 框架(如 Spring、Ktor 等)至关重要。
练习
- 创建一个
@Log注解,标记需要记录日志的方法 - 使用反射读取自定义注解,实现一个简单的依赖注入容器
- 创建
@Validate注解,结合反射实现参数验证 - 实现一个使用注解的路由映射器
- 使用 KSP 或 KAPT 创建一个简单的注解处理器