注解

注解用于给代码元素附加元数据。编译器、框架、代码生成器、测试工具、依赖注入容器、序列化库都可能读取这些元数据并改变行为。

Java 开发者对注解通常很熟悉,但 Kotlin 的属性、主构造函数、use-site target 和文件级注解会带来一些新的细节。

声明注解

annotation class Fancy

使用:

@Fancy
class User

注解本身是一种特殊的类,用 annotation class 声明。

元注解

可以用元注解限制注解的用途:

@Target(
    AnnotationTarget.CLASS,
    AnnotationTarget.FUNCTION,
    AnnotationTarget.VALUE_PARAMETER
)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Audit

常见元注解:

  • @Target:允许标注哪些元素。
  • @Retention:注解保留到源码、字节码还是运行时。
  • @Repeatable:允许重复使用同一个注解。
  • @MustBeDocumented:表示它属于公共 API 文档的一部分。

注解参数

annotation class Route(val path: String, val method: String = "GET")

@Route("/users")
class UserController

注解参数允许的类型包括:

  • 基本类型对应类型,例如 IntLong
  • String
  • 类引用,例如 User::class
  • 枚举。
  • 其他注解。
  • 上述类型的数组。

注解参数不能是可空类型,因为 JVM 注解属性不能存储 null

类引用参数

import kotlin.reflect.KClass

annotation class HandlerFor(val type: KClass<*>)

@HandlerFor(User::class)
class UserHandler

Kotlin 中用 KClass 表达类引用。编译到 JVM 后,Java 仍能按普通 annotation class 参数读取。

注解主构造函数

如果要注解主构造函数,需要显式写 constructor

class Service @Inject constructor(
    private val repository: Repository
)

如果只是注解构造参数:

class Service(
    @Named("main") private val repository: Repository
)

注解属性访问器

class User {
    var name: String = ""
        @Inject set
}

这类写法不常见,但在依赖注入、序列化、Java Bean 框架互操作中可能遇到。

use-site target

Kotlin 一个属性可能在 JVM 上生成多个元素:

  • 构造函数参数。
  • backing field。
  • getter。
  • setter。
  • Kotlin property 元数据。

因此有时必须明确注解应该放到哪里:

data class User(
    @field:Email
    val email: String
)

常见 target:

target 含义
@field: Java 字段
@get: getter
@set: setter
@param: 构造函数参数
@property: Kotlin 属性,Java 不可见
@file: 文件
@receiver: 扩展函数或属性的接收者
@delegate: 委托属性保存委托对象的字段

Bean Validation 示例

Java 生态中的校验注解经常要求打到字段或 getter 上。Kotlin 写法要根据框架读取位置选择:

data class RegisterRequest(
    @field:Email
    @field:NotBlank
    val email: String
)

如果写成:

data class RegisterRequest(
    @Email
    val email: String
)

编译器会按规则选择默认目标,但这个默认目标不一定是框架读取的位置。对 Java 框架互操作时,显式写 @field:@get: 更稳。

文件级注解

文件级注解放在 package 之前:

@file:JvmName("StringUtils")

package demo

fun normalize(value: String) = value.trim()

这会影响顶层函数在 Java 中生成的类名。

all meta-target

Kotlin 的 @all: 可以把注解传播到多个相关位置,例如构造参数、属性、字段、getter、setter 参数等:

data class User(
    @all:Email
    val email: String
)

这对 Java records、Bean Validation 等场景有帮助。但它也有限制,例如不能用于委托属性,不能用 @all:[A B] 一次包多个注解。

Java 注解在 Kotlin 中使用

Java:

public @interface Ann {
    int intValue();
    String stringValue();
}

Kotlin 必须用命名参数:

@Ann(intValue = 1, stringValue = "abc")
class Demo

如果 Java 注解参数名是 value,可以省略参数名:

@AnnWithValue("abc")
class Demo

数组参数:

@AnnWithArrayMethod(names = ["a", "b", "c"])
class Demo

重复注解

@Repeatable
annotation class Tag(val name: String)

@Tag("api")
@Tag("stable")
class UserApi

Kotlin 和 Java 都支持 repeatable annotations,但生成容器注解的机制细节不同。需要与 Java 框架深度互操作时,关注 @JvmRepeatable

KSP 与注解处理

如果你要基于注解做代码生成,Kotlin 官方更推荐 Kotlin Symbol Processing,也就是 KSP。相比 Java 注解处理器,KSP 更理解 Kotlin 语法和符号模型,例如属性、扩展、空性、默认参数等。

Java 的 APT/KAPT 仍然可用,但在纯 Kotlin 项目中,优先评估 KSP。

实践建议

  • 与 Java 框架互操作时,显式使用 @field:@get:@param:
  • 注解参数不能可空,默认值要认真设计。
  • 文件级注解必须放在 package 前。
  • 自定义注解时明确 @Retention,避免运行时读取不到。
  • 新的 Kotlin 代码生成优先考虑 KSP。

参考

  • 官方注解文档:https://kotlinlang.org/docs/annotations.html