公众号「稀有猿诉」 原文链接 Kotlin Annotation Made Easy
注解(Annotations)允许我们在代码中添加元数据(Meta data),提供代码以外的信息,这些元数据可以在编译时被编译器或其他工具读取和处理。 Kotlin作为一种功能强大且易于使用的多范式通用编程语言,注解(Annotations)是其核心特性之一。在Kotlin中,注解的使用非常广泛,可以用于框架设计、代码生成、测试、依赖注入等多个方面。今天就来学习一下Kotlin中注解的使用方法。
Kotlin是基于JVM的编程语言,并且可以与Java互通使用,因此事先了解一下Java的注解对于学习Kotlin的注解是非常有帮助的。可以阅读一下前面的文章来回顾Java语言的注解。
什么是注解
注解是元编程的一种实现方式,它并不直接改变代码,而是为代码提供额外的数据。注解不能单独存在,必须与代码中的其他元素一起使用。在Kotlin中,注解要使用符号『@』后面加一个已定义的注解名字,如『@Deprecated』。注解在Kotlin中的使用非常广泛的,相信有过代码经验的同学都至少看过大量的注解。
注解的使用方法
注解的使用是非常的直观的,在需要的代码元素(类,变量,属性,函数,参数等等)加上想要使用的注解就可以了:
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
Kotlin的注解也可以用在lambda上面,这实际上相当于应用于lambda函数生成的函数实例的invoke()上面:
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
注解的使用点目标
由于Kotlin最终要编译成为字节码,运行在JVM上,所以它必须符合Java的规范。但语法上Kotlin与Java还是不一样的,比如一句Kotlin代码可能会相当于Java的好几句,换句话说一个Kotlin语句中的元素可能会对应着Java中的好几个。这可能会带来问题。
注解并不能单独出现,它必须作用到某一个语法上的元素,因为Kotlin语法元素可能会对应着几个Java语法元素,那么注解可能会被用在多个目标元素上面。为了能精确的指定注解的作用目标,可以使用『使用点目标』(use-site targets)来标记具体的目标元素:
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』指的是扩展函数发生作用的实例,比如说:
fun @receiver:Fancy String.myExtension() { ... }
那么,这个注解『Fancy』将作用于具体调用这个扩展方法myExtension的String实例上面。
这些具体的使用点目标可以精确的指定JVM认识的元素上面,可以发现,它们远比定义注解时的@Target要丰富。如果不指定具体的使用点目标,那么就会按照@Target指定的目标,如果有多个目标,会按如下顺序选择:
- param
- property
- field
兼容Java注解
Kotlin是完全兼容Java注解,也就是说Java中定义的注解,在Kotlin中都可以直接使用。
// Java
public @interface Ann {
int intValue();
String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C
虽然可以直接用,但毕竟Kotlin的语法要丰富得多,所以为了避免歧义,要使用前面介绍的使用点目标来精确指定注解的作用目标。
自定义注解
使用关键字『annotation』来声明自定义注解,如:
annotation class Fancy
之后就可以使用注解了:
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
光这样声明还不够,还需要定义注解具体的内容,如可修饰的目标和行为特点,这就需要用到元注解(Meta annotations),也即定义注解时所需要的注解。
元注解(Meta annotations)
@MustBeDocumented
用于指定此注解是公开API的一部分,必须包含在文档中。
@Repeatable
允许在同一个地方多次使用注解。
@Target
用于指定此注解可以应用到哪些程序元素上面,如类和接口,函数,属性和表达式。
- AnnotationTarget.CLASS - 类型,包括类型原型(classes),接口,对象,注解类型
- AnnotationTarget.PROPERTY - 属性
- AnnotationTarget.FIELD - 域变量
- AnnotationTarget.LOCAL_VARIABLE - 局部变量(本地变量)
- AnnotationTarget.VALUE_PARAMETER - 参数
- AnnotationTarget.CONSTRUCTOR - 构造函数
- AnnotationTarget.FUNCTION - 函数
- AnnotationTarget.PROPERTY_GETTER - 属性的getter
- AnnotationTarget.PROPERTY_SETTER - 属性的setter
@Retention
指定注解信息保存到代码生命周期的哪一阶段,编译前,编译时还是运行时。默认值是运行时,也即在运行时注解是可见的。
- AnnotationRetention.SOURCE - 只在源码过程中保留,并不会出现在编译后的class中(二进制文件中)。
- AnnotationRetention.BINARY - 会在class中保留,但对于运行时并不可见,也就是通过反射无法得到注解。
- AnnotationRetention.RUNTIME - 注解会保留到运行时,运行时的操作如反射可以解析注解,这是默认的@Rentention值。
构造方法(Constructors)
与Java很不同的是Kotlin的注解更加的像常规的类(class),注解也可以有构造函数:
annotation class Special(val why: String)
@Special("example") class Foo {}
构造函数可以使用的参数包括:
- 基础数据类型Int,Long,Float和String等
- 类型原型(即class,如Foo::class)
- 枚举类型
- 其他注解类型
- 由以上类型组成的数组
注意不能有可能为空(如String?)的类型,当然也不可以传递null给注解的构造函数。还有,如果用其他注解作为参数时,注解名字前就不用再加『@』了:
annotation class ReplaceWith(val expression: String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""))
注解的实例化(Instantiation)
在Kotlin中可以通过调用注解的构造函数来实例化一个注解来使用。而不必非要像Java那样用反射接口去获取。
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注解解析可以参考另一篇文章,因为运行时注解解析用处并不大,并且也不复杂,看一个简单🌰就可以了:
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),并且要配合编译器插件kapt和KSP来使用。
需要注意,因为注解是JVM支持的特性,在编译时需要借助javac编译器,所以只有运行目标是JVM时注解才有效。因为Kotlin是支持编译为不同运行目标的,除了JVM外,还有JavaScript和Native。
实现注解处理器
与Java的注解处理器类似,在定义好注解后,还需要实现一个注解处理器,以对注解进行处理。一般情况下实现AbstractProcessor就可以了。在其process方法中过滤出来想要处理的注解进行处理,比如使用KotlinPoet生成代码。
另外,还要注意,注解处理器必须在一个单独的module中,然后添加为使用此注解module的依赖,这是因为注解的处理是在编译前,所以处理器需要在正式编译前就已经编译好。
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』,然后把实现的注解处理器的完整类名,写在这个文件的第一行:
// file: resources/META-INF/services/javax.annotation.processing.Processor
net.toughcoder.MyAnnotationProcessor
使用注解处理器
需要做两个事情,一个是把注解处理器添加为其他项目或者module的依赖。然后再用专门处理注解处理器的编译器插件使用注解处理器。
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编程中的核心特性,它可以帮助我们提高代码的可读性、可维护性和可扩展性。大部分的注解都在编译时,也不会对性能产生影响,所以可以放心大胆的用注解来提升开发效率。
参考资料
- Annotations
- Kotlin Annotations
- Annotation Processing
- Annotation Processing: Supercharge Your Development
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」,「点赞」,「在看」,「收藏」,「分享」 总要有一个吧!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。