友情提醒:如果没搞过注解处理器,这篇文章你看起来可能会比较迷。。
话说,最近尝试了一下写了个注解处理器,也就是我们常见的 apt,在 Kotlin 当中有个插件叫 kapt,说的就是注解处理器。注解处理器能干什么呢?能帮我们生成一些代码,让我们变懒,让我们的代码变优雅(也许吧)。
需要注意的是,这个注解处理器是 Java 编译器的特性,而 Java 编译器根本不知道 Kotlin 是神马东西,于是乎,如果大家在 Android 当中用到了 kapt 这个插件,你就会发现在 build/tmp/kapt3 下面有个 stubs 目录:
这个目录里面会有从你的 Kotlin 源码生成的 Java 源码,注解处理器后面会跟据这些源码去做注解处理,这实际上就是 kapt 的原理啦,如果你之前看到过官方写的介绍 kapt 原理的文章,里面说的 stubs ,就是这个。
话说到这儿,不得不提一句,既然注解处理器是 Java 编译器的特性,于是乎,kotlinjs/kotlin native 是没有这一项功能的。
我们写注解处理器,需要编写一个配置文件让编译器知道哪个是注解处理器的入口:
大家看到图中这个文件是红色的,在 IntelliJ 当中红色的目录都是编译生成的,所以这个文件对于偷懒的人来说也根本不会去手写它,而是用 AutoService
。
@AutoService(Processor.class)
public class AJavaProcessor extends AbstractProcessor {
...
}
当然,这个需要引入依赖的:
implementation 'com.google.auto.service:auto-service:1.0-rc4'
其实这货呢,也是一个注解处理器,帮我们在编译的时候生成注解处理器相应的配置文件。
需要注意的是,如果你的注解处理器入口代码是用 Kotlin 写的,那么 AutoService
就傻了。
@AutoService(Processor::class)
class AKotlinProcessor: AbstractProcessor() {
...
}
为什么呢?显然直接通过上面的这种依赖方式,只会让 Javac 知道有这么个注解处理器,而 Javac 哪里知道还有什么叫 Kotlin 的东西啊,所以我们还得让 kapt 知道才行。
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
dependencies {
...
kapt 'com.google.auto.service:auto-service:1.0-rc4'
implementation 'com.google.auto.service:auto-service:1.0-rc4'
...
}
首先我们要添加 Kotlin 的各种插件,然后在依赖当中用 kapt 引入google 的 AutoService
,又由于 AutoService
中的注解依赖也在这个包里,所以我们还是要把它添加到运行时依赖的(kapt 下面 implementation 那句)。
有了上面的配置,那么我们首先就会在前面提到的 build/tmp/kapt3/stubs 目录中找到我们用 Kotlin 编写的代码转成的 Java 代码,其次 AutoService
生成的注解处理器的配置也会跑到 kapt3/classes 中(原来是在 build/classes/java/main 中)
既然都是 Java 文件,那么我怎么在注解处理器内识别出来哪些代码是 Java 的,哪些是 Kotlin 的呢?其实这个也不难,对比一下就知道了,给大家看一个例子,我有一个 Kotlin 写的类:
class Hello {
}
生成的 stub 长这样:
@kotlin.Metadata(mv = {1, 1, 9}, bv = {1, 0, 2}, k = 1, d1 = {"..."}, d2 = {"Lcom/bennyhuo/activitybuilder/Hello;", "", "()V", "app_debug"})
public final class Hello {
public Hello() {
super();
}
}
哈哈,一眼就看出来,那个注解,什么鬼,Java 源码肯定不会有的。所以要识别你所处理的类是不是 Kotlin 编写的,只需要:
Metadata metadata = typeElement.getAnnotation(Metadata.class);
//如果有这个注解,说明就是 Kotlin 类。
boolean isKotlin = metadata != null;
一旦能够识别出来注解标注的类是 Kotlin,那么我们就可以采用一些 Kotlin Style 的方式生成代码,例如本来如果是 Java 源码,我会生成这样的一个方法:
public class HelloHelper{
public static void toHelloString(Hello hello){
...
}
}
如果我处理的是 Kotlin 源码,我完全可以生成一个扩展方法让 Kotlin 开发者更愉快地调用:
fun Hello.toHelloString(){
...
}
当然,这个扩展方法也是可以被 Java 开发者很愉快地调用的。
我们一再提到注解处理器只认识 Java,所以就算你用 Kotlin 定义了一个方法如下:
fun hello(a: Int, b: String){
...
}
如果我们用注解处理器处理它的时候,参数 a
的类型就会变成 Java 的 int.class
或者 Integer.class
,而参数 b
的类型则会变成 java.lang.String
,注意不是 kotlin.String
。
如果你要根据这些类型对应地去生成代码,你需要将这些类型做映射,例如:
java.lang.String -> kotlin.String
java.lang.Integer -> kotlin.Int
int -> kotlin.Int
这个要怎么办呢?不能怎么办,连 J 神的 Kotlin Poet 都没有做这件事儿,如果我们需要写注解处理器生成 Kotlin 的代码,这一点你需要自己来处理。不过呢,我可以给大家一点儿提示,实际上这个类型转换 Kotlin 编译器是做了的,具体可以参考编译器源码:
object JavaToKotlinClassMap : PlatformToKotlinClassMap {
private val javaToKotlin = HashMap<FqNameUnsafe, ClassId>()
...
}
这个 HashMap
当中就存放了需要映射的类型。
其实我们前面提到了,用 J 神的 Kotlin Poet 这个项目生成 Kotlin 源码的体验几乎与 Java Poet 没差。不过呢,这个项目目前还只是发到了 0.6,所以难免有个小 bug 啥的,例如我要生成一个匿名内部类,就算我只实现了一个接口,它也会给我添加一个构造方法调用的括号:
object: SomeInterface(){
...
}
这样是不对滴。不过这个问题呢,显然也不是什么大问题,已经有大神给了 fix:
Correcting handling of super-classes/interfaces on anonymous classes
https://github.com/square/kotlinpoet/pull/316
由于这个库目前还不算太成熟,参考资料不多,所以如果你想要用,最好去参考一下其中的 test case 来了解其用法。
简单来说,为 Kotlin 提供 apt 服务,无论从编译器(kapt)还是从注解处理器的开发者来讲,你必须都得装作你写的和用的都是 Java 才行。