5.1.4 注解

原文链接: https://kotlinlang.org/docs/annotations.html

5.1.4 注解

注解是可以用来为代码元素附加元数据的标签。工具和框架会在编译期和运行期处理这些元数据,并据此执行不同的操作。

你可以用注解来简化和自动化常见任务,例如生成样板代码、强制编码规范或编写文档。

提示: 如果你想开发自己的注解处理器,可以使用 Kotlin 符号处理(KSP) API。

声明

注解是一种特殊的类。要声明注解,请在类声明之前使用 annotation 关键字:

1
annotation class Fancy

注解的额外属性可以通过为注解类添加元注解来指定:

  • @Target 指定可以被该注解标注的元素种类(例如类、函数、属性和表达式);
  • @Retention 指定该注解是否存储在编译后的 class 文件中,以及它在运行期是否可以通过反射访问(默认两者都为真);
  • @Repeatable 允许在同一个元素上多次使用同一个注解;
  • @MustBeDocumented 指定该注解是公开 API 的一部分,应当包含在生成的 API 文档所展示的类或方法签名中。
1
2
3
4
5
6
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
        AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER,
        AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy

用法

1
2
3
4
5
@Fancy class Foo {
    @Fancy fun baz(@Fancy foo: Int): Int {
        return (@Fancy 1)
    }
}

如果你需要标注类的主构造器,请在构造器声明中添加 constructor 关键字,并把注解写在它前面:

1
class Foo @Inject constructor(dependency: MyDependency) { ... }

你也可以标注属性访问器:

1
2
3
4
class Foo {
    var x: MyDependency? = null
        @Inject set
}

构造器

注解可以有带参数的构造器。

1
2
3
annotation class Special(val why: String)

@Special("example") class Foo {}

允许的参数类型有:

  • 与 Java 基本类型对应的类型(Int、Long 等)
  • 字符串
  • 类(Foo::class)
  • 枚举
  • 其他注解
  • 上述类型的数组

注解参数不能使用可空类型,因为 JVM 不支持把 null 存储为注解属性的值。

如果一个注解被用作另一个注解的参数,它的名称前面不加 @ 字符:

1
2
3
4
5
6
7
annotation class ReplaceWith(val expression: String)

annotation class Deprecated(
        val message: String,
        val replaceWith: ReplaceWith = ReplaceWith(""))

@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))

如果你需要把一个类指定为注解的实参,请使用 Kotlin 类(KClass)。Kotlin 编译器会自动把它转换为 Java 类,这样 Java 代码就能正常访问这些注解和实参。

1
2
3
4
5
6

import kotlin.reflect.KClass

annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)

@Ann(String::class, Int::class) class MyClass

实例化

在 Java 中,注解类型是接口的一种形式,因此你可以实现它并使用实例。作为这种机制的替代方案,Kotlin 允许你在任意代码中调用注解类的构造器,并同样使用得到的实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
annotation class InfoMarker(val info: String)

fun processInfo(marker: InfoMarker): Unit = TODO()

fun main(args: Array<String>) {
    if (args.isNotEmpty())
        processInfo(getAnnotationReflective(args))
    else
        processInfo(InfoMarker("default"))
}

关于注解类实例化的更多内容,请参阅这个 KEEP。

Lambda

注解也可以用在 lambda 上。它们会被应用到生成 lambda 函数体的 invoke() 方法上。这对 Quasar 之类使用注解进行并发控制的框架很有用。

1
2
3
annotation class Suspendable

val f = @Suspendable { Fiber.sleep(10) }

注解使用处目标

当你标注属性或主构造器参数时,相应的 Kotlin 元素会生成多个 Java 元素,因此在生成的 Java 字节码中,注解可能有多个可放置的位置。要明确指定注解应如何生成,请使用以下语法:

1
2
3
class Example(@field:Ann val foo,    // annotate only the Java field
              @get:Ann val bar,      // annotate only the Java getter
              @param:Ann val quux)   // annotate only the Java constructor parameter

同样的语法也可以用来标注整个文件。为此,请把目标为 file 的注解放在文件的顶层,位于 package 指令之前;如果文件位于默认包中,则放在所有 import 之前:

1
2
3
@file:JvmName("Foo")

package org.jetbrains.demo

如果你有多个目标相同的注解,可以在目标后面加上方括号,把所有注解放在方括号里,从而避免重复书写目标(all 元目标除外):

1
2
3
4
class Example {
     @set:[Inject VisibleForTesting]
     var collaborator: Collaborator
}

支持的使用处目标的完整列表如下:

  • file
  • field
  • property(使用此目标的注解对 Java 不可见)
  • get(属性 getter)
  • set(属性 setter)
  • all(属性的元目标,更多信息请参阅 all 元目标一节)
  • receiver(扩展函数或属性的接收者参数)

要标注扩展函数的接收者参数,请使用以下语法:

1
    fun @receiver:Fancy String.myExtension() { ... }
  • param(构造器参数)
  • setparam(属性 setter 参数)
  • delegate(为委托属性存储委托实例的字段)

未指定使用处目标时的默认行为

如果你没有指定使用处目标,编译器会根据你所使用注解的 @Target 注解来选择目标。如果有多个适用目标,编译器会按以下顺序选择一个或多个:

  • 构造器参数目标(param)。
  • 属性目标(property)。
  • 字段目标(field),前提是它适用而属性目标(property)不适用。

如果 param、property 和 field 都不适用,该注解就是无效的,你需要显式指定使用处目标。

我们以 Jakarta Bean Validation 的 @Email 注解为例:

1
2
@Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE})
public @interface Email { }

使用这个注解时,请看下面的示例:

1
2
3
4
5
6
data class User(val username: String,
                // @Email is now equivalent to @param:Email @field:Email
                @Email val email: String) {
    // @Email is still equivalent to @field:Email
    @Email val secondaryEmail: String? = null
}

在这个示例中,@Email 注解会同时应用到 email 属性的构造器参数目标和字段目标,因为该属性:

  • 声明在主构造器中。
  • 没有自定义的 getter 或 setter,因此编译器会生成幕后字段。

而对于 secondaryEmail 属性,@Email 注解只会应用到字段目标,因为该属性:

  • 没有声明在主构造器中。
  • 没有自定义的 getter 或 setter,因此编译器会生成幕后字段。

all 元目标

all 目标让你可以更方便地把同一个注解不仅应用到参数和属性或字段上,还应用到相应的 getter 和 setter 上。

具体来说,标记为 all 的注解会在适用时传播到:

  • 构造器参数(param),前提是该属性定义在主构造器中。
  • 属性本身(property)。
  • 幕后字段(field),前提是该属性有幕后字段。
  • getter(get)。
  • setter 参数(setparam),前提是该属性定义为 var。
  • 仅 Java 的目标 RECORD_COMPONENT,前提是该类带有 @JvmRecord 注解。

我们以 Jakarta Bean Validation 的 @Email 注解为例,它的定义如下:

1
2
@Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE})
public @interface Email { }

在下面的示例中,这个 @Email 注解被应用到了所有相关的目标上:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
data class User(
    val username: String,
    // Applies @Email to param, field, and get
    @all:Email val email: String,
    // Applies @Email to param, field, get, and setparam
    @all:Email var name: String,
) {
    // Applies @Email to field and getter (not param since it's not in the constructor)
    @all:Email val secondaryEmail: String? = null
}

你可以对任何属性使用 all 元目标,无论它在主构造器内还是外。

限制

all 目标有一些限制:

  • 它不会把注解传播到类型、可能的扩展接收者,或上下文接收者与上下文参数。
  • 它不能与多个注解一起使用:
1
2
    @all:[A B] // forbidden, use @all:A @all:B
    val x: Int = 5

Java 注解

Java 注解与 Kotlin 100% 兼容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.junit.Test
import org.junit.Assert.*
import org.junit.Rule
import org.junit.rules.*

class Tests {
    // apply @Rule annotation to property getter
    @get:Rule val tempFolder = TemporaryFolder()

    @Test fun simple() {
        val f = tempFolder.newFile()
        assertEquals(42, getTheAnswer())
    }
}

由于用 Java 书写的注解并未定义参数顺序,你不能用普通的函数调用语法来传参。相反,你需要使用具名参数语法:

1
2
3
4
5
// Java
public @interface Ann {
    int intValue();
    String stringValue();
}
1
2
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C

与 Java 一样,value 参数是一个特例:它的值可以在不显式写出参数名的情况下指定:

1
2
3
4
// Java
public @interface AnnWithValue {
    String value();
}
1
2
// Kotlin
@AnnWithValue("abc") class C

数组作为注解参数

如果 Java 中的 value 实参是数组类型,它在 Kotlin 中会变成 vararg 参数:

1
2
3
4
// Java
public @interface AnnWithArrayValue {
    String[] value();
}
1
2
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C

对于其他数组类型的实参,你需要使用数组字面量语法或 arrayOf(...):

1
2
3
4
// Java
public @interface AnnWithArrayMethod {
    String[] names();
}
1
2
@AnnWithArrayMethod(names = ["abc", "foo", "bar"])
class C

访问注解实例的属性

注解实例的值会以属性的形式暴露给 Kotlin 代码:

1
2
3
4
// Java
public @interface Ann {
    int value();
}
1
2
3
4
// Kotlin
fun foo(ann: Ann) {
    val i = ann.value
}

不生成 JVM 1.8+ 注解目标的能力

如果某个 Kotlin 注解的 Kotlin 目标中包含 TYPE,那么该注解在它的 Java 注解目标列表中会映射为 java.lang.annotation.ElementType.TYPE_USE。这就像 Kotlin 的 TYPE_PARAMETER 目标会映射为 java.lang.annotation.ElementType.TYPE_PARAMETER 一样。对于 API 级别低于 26 的 Android 客户端来说,由于它们的 API 中没有这些目标,这就会带来问题。

要避免生成 TYPE_USE 和 TYPE_PARAMETER 注解目标,请使用新的编译器参数 -Xno-new-java-annotation-targets。

可重复注解

与 Java 一样,Kotlin 也有可重复注解,它们可以在同一个代码元素上应用多次。要让你的注解可重复,请用 @kotlin.annotation.Repeatable 元注解标记其声明。这样它在 Kotlin 和 Java 中都可重复。Kotlin 一侧同样支持 Java 的可重复注解。

与 Java 中所用方案的主要区别在于没有容器注解,Kotlin 编译器会自动用一个预定义的名称生成它。对于下面示例中的注解,它会生成容器注解 @Tag.Container:

1
2
3
4
@Repeatable
annotation class Tag(val name: String)

// The compiler generates the @Tag.Container containing annotation

你可以通过应用 @kotlin.jvm.JvmRepeatable 元注解,并传入一个显式声明的容器注解类作为实参,来为容器注解设置自定义名称:

1
2
3
4
@JvmRepeatable(Tags::class)
annotation class Tag(val name: String)

annotation class Tags(val value: Array<Tag>)

要通过反射提取 Kotlin 或 Java 的可重复注解,请使用 KAnnotatedElement.findAnnotations() 函数。

关于 Kotlin 可重复注解的更多内容,请参阅这个 KEEP。