稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Kotlin Annotation Made Easy

注解(Annotations)允许我们在代码中添加元数据(Meta data),提供代码以外的信息,这些元数据可以在编译时被编译器或其他工具读取和处理。 Kotlin作为一种功能强大且易于使用的多范式通用编程语言,注解(Annotations)是其核心特性之一。在Kotlin中,注解的使用非常广泛,可以用于框架设计、代码生成、测试、依赖注入等多个方面。今天就来学习一下Kotlin中注解的使用方法。

Kotlin是基于JVM的编程语言,并且可以与Java互通使用,因此事先了解一下Java的注解对于学习Kotlin的注解是非常有帮助的。可以阅读一下前面的文章来回顾Java语言的注解

什么是注解

注解是元编程的一种实现方式,它并不直接改变代码,而是为代码提供额外的数据。注解不能单独存在,必须与代码中的其他元素一起使用。在Kotlin中,注解要使用符号『@』后面加一个已定义的注解名字,如『@Deprecated』。注解在Kotlin中的使用非常广泛的,相信有过代码经验的同学都至少看过大量的注解。

注解的使用方法

注解的使用是非常的直观的,在需要的代码元素(类,变量,属性,函数,参数等等)加上想要使用的注解就可以了:

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

Kotlin的注解也可以用在lambda上面,这实际上相当于应用于lambda函数生成的函数实例的invoke()上面:

1
2
3
annotation class Suspendable

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

注解的使用点目标

由于Kotlin最终要编译成为字节码,运行在JVM上,所以它必须符合Java的规范。但语法上Kotlin与Java还是不一样的,比如一句Kotlin代码可能会相当于Java的好几句,换句话说一个Kotlin语句中的元素可能会对应着Java中的好几个。这可能会带来问题。

注解并不能单独出现,它必须作用到某一个语法上的元素,因为Kotlin语法元素可能会对应着几个Java语法元素,那么注解可能会被用在多个目标元素上面。为了能精确的指定注解的作用目标,可以使用『使用点目标』(use-site targets)来标记具体的目标元素:

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

这里面『Ann』是一个注解,其前面的『field/get/param』就用以指定具体的注解目标元素。可用的使用点目标有这些:

  • file
  • property
  • field
  • get 属性的getter
  • set 属性的setter
  • receiver 扩展函数或者扩展属性的底层对象
  • param 构造函数的参数
  • setparam 属性setter的参数
  • delegate 指存储着受托对象实例的域成员

『receiver』指的是扩展函数发生作用的实例,比如说:

1
fun @receiver:Fancy String.myExtension() { ... }

那么,这个注解『Fancy』将作用于具体调用这个扩展方法myExtension的String实例上面。

这些具体的使用点目标可以精确的指定JVM认识的元素上面,可以发现,它们远比定义注解时的@Target要丰富。如果不指定具体的使用点目标,那么就会按照@Target指定的目标,如果有多个目标,会按如下顺序选择:

  • param
  • property
  • field

兼容Java注解

Kotlin是完全兼容Java注解,也就是说Java中定义的注解,在Kotlin中都可以直接使用。

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

虽然可以直接用,但毕竟Kotlin的语法要丰富得多,所以为了避免歧义,要使用前面介绍的使用点目标来精确指定注解的作用目标。

自定义注解

使用关键字『annotation』来声明自定义注解,如:

1
annotation class Fancy

之后就可以使用注解了:

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

光这样声明还不够,还需要定义注解具体的内容,如可修饰的目标和行为特点,这就需要用到元注解(Meta annotations),也即定义注解时所需要的注解。

元注解(Meta annotations)

@MustBeDocumented

用于指定此注解是公开API的一部分,必须包含在文档中。

@Repeatable

允许在同一个地方多次使用注解。

@Target

用于指定此注解可以应用到哪些程序元素上面,如类和接口,函数,属性和表达式。

@Retention

指定注解信息保存到代码生命周期的哪一阶段,编译前,编译时还是运行时。默认值是运行时,也即在运行时注解是可见的。

  • AnnotationRetention.SOURCE - 只在源码过程中保留,并不会出现在编译后的class中(二进制文件中)。
  • AnnotationRetention.BINARY - 会在class中保留,但对于运行时并不可见,也就是通过反射无法得到注解。
  • AnnotationRetention.RUNTIME - 注解会保留到运行时,运行时的操作如反射可以解析注解,这是默认的@Rentention值。

构造方法(Constructors)

与Java很不同的是Kotlin的注解更加的像常规的类(class),注解也可以有构造函数:

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

@Special("example") class Foo {}

构造函数可以使用的参数包括:

  • 基础数据类型Int,Long,Float和String等
  • 类型原型(即class,如Foo::class)
  • 枚举类型
  • 其他注解类型
  • 由以上类型组成的数组

注意不能有可能为空(如String?)的类型,当然也不可以传递null给注解的构造函数。还有,如果用其他注解作为参数时,注解名字前就不用再加『@』了:

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

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

注解的实例化(Instantiation)

在Kotlin中可以通过调用注解的构造函数来实例化一个注解来使用。而不必非要像Java那样用反射接口去获取。

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"))
}

注解解析

Kotlin是基于JVM的编程语言,最终要编译成为字节码运行在JVM上面,所以注解的解析与Java语言注解解析是一样的,可以在运行时用反射API来解析注解。关于Java注解解析可以参考另一篇文章,因为运行时注解解析用处并不大,并且也不复杂,看一个简单🌰就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
class Item(
  @Positive val amount: Float,
  @AllowedNames(["Alice", "Bob"]) val name: String)

val fields = item::class.java.declaredFields
for (field in fields) {
    for (annotation in field.annotations) {
        if (field.isAnnotationPresent(AllowedNames::class.java)) {
            val allowedNames = field.getAnnotation(AllowedNames::class.java)?.names
         }
    }
}

注解处理器

注解是元编程的一种方式,它最大的威力是在编译前进行代码处理和代码生成。除了注解的定义和使用外,更为关键的注解的处理需要用到注解处理器(Annotation Processor),并且要配合编译器插件kaptKSP来使用。

需要注意,因为注解是JVM支持的特性,在编译时需要借助javac编译器,所以只有运行目标是JVM时注解才有效。因为Kotlin是支持编译为不同运行目标的,除了JVM外,还有JavaScript和Native。

实现注解处理器

与Java的注解处理器类似,在定义好注解后,还需要实现一个注解处理器,以对注解进行处理。一般情况下实现AbstractProcessor就可以了。在其process方法中过滤出来想要处理的注解进行处理,比如使用KotlinPoet生成代码。

另外,还要注意,注解处理器必须在一个单独的module中,然后添加为使用此注解module的依赖,这是因为注解的处理是在编译前,所以处理器需要在正式编译前就已经编译好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package net.toughcoder

import javax.annotation.processing.*
import javax.lang.model.element.*
import javax.tools.Diagnostic

@SupportedAnnotationTypes("com.example.MyAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class MyAnnotationProcessor : AbstractProcessor() {

    override fun process(annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
        for (annotation : annotations) {
            for (element : roundEnv.getElementsAnnotatedWith(annotation)) {
                val myAnnotation = element.getAnnotation(MyAnnotation::class.java)
                val message = "Processing element with annotation MyAnnotation(value = ${myAnnotation.value})"
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message, element)
            }
        }
        return true
    }
}

从例子中可以看到,其实Kotlin中的注解处理器(Processor)直接就是用的Java的,所以在用的时候最好加上Java语言的版本。

注册注解处理器

为能正常使用注解处理器,需要把注解处理器放在一个单独的Module里,并作为其他module的依赖,这样能确保它在编译被依赖项时正常使用,被依赖项也即注解使用的地方。

需要在处理器module中与代码平级的文件夹创建resources文件夹,创建一个子文件夹META-INF,再在META-INF创建一个子文件services,在里面创建一个文件名为『javax.annotation.processing.Processor』,然后把实现的注解处理器的完整类名,写在这个文件的第一行:

1
2
// file: resources/META-INF/services/javax.annotation.processing.Processor
net.toughcoder.MyAnnotationProcessor

使用注解处理器

需要做两个事情,一个是把注解处理器添加为其他项目或者module的依赖。然后再用专门处理注解处理器的编译器插件使用注解处理器。

1
2
3
4
5
6
7
8
9
dependencies {
    implementation(kotlin('stdlib'))
    kapt 'net.toughcoder:my-annotation-processor:1.0.0'
}

kapt {
    useBuildCache = true
    annotationProcessors = ['net.toughcoder:my-annotation-processor:1.0.0']
}

总结

本文介绍了Kotlin中注解的基本语法、使用方法和处理过程。通过自定义注解处理器,我们可以在编译时处理注解并生成相应的代码或执行其他任务。注解是Kotlin编程中的核心特性,它可以帮助我们提高代码的可读性、可维护性和可扩展性。大部分的注解都在编译时,也不会对性能产生影响,所以可以放心大胆的用注解来提升开发效率。

参考资料

Comments