如何让注解处理器支持 Kotlin?

2020-02-20 13:19:10 浏览数 (1)

友情提醒:如果没搞过注解处理器,这篇文章你看起来可能会比较迷。。

什么是注解处理器

话说,最近尝试了一下写了个注解处理器,也就是我们常见的 apt,在 Kotlin 当中有个插件叫 kapt,说的就是注解处理器。注解处理器能干什么呢?能帮我们生成一些代码,让我们变懒,让我们的代码变优雅(也许吧)。

需要注意的是,这个注解处理器是 Java 编译器的特性,而 Java 编译器根本不知道 Kotlin 是神马东西,于是乎,如果大家在 Android 当中用到了 kapt 这个插件,你就会发现在 build/tmp/kapt3 下面有个 stubs 目录:

这个目录里面会有从你的 Kotlin 源码生成的 Java 源码,注解处理器后面会跟据这些源码去做注解处理,这实际上就是 kapt 的原理啦,如果你之前看到过官方写的介绍 kapt 原理的文章,里面说的 stubs ,就是这个。

话说到这儿,不得不提一句,既然注解处理器是 Java 编译器的特性,于是乎,kotlinjs/kotlin native 是没有这一项功能的。

为什么 AutoService 不认识 Kotlin 写的 Processor?

我们写注解处理器,需要编写一个配置文件让编译器知道哪个是注解处理器的入口:

大家看到图中这个文件是红色的,在 IntelliJ 当中红色的目录都是编译生成的,所以这个文件对于偷懒的人来说也根本不会去手写它,而是用 AutoService

代码语言:javascript复制
@AutoService(Processor.class)
public class AJavaProcessor extends AbstractProcessor {
    ...
}

当然,这个需要引入依赖的:

代码语言:javascript复制
implementation 'com.google.auto.service:auto-service:1.0-rc4'

其实这货呢,也是一个注解处理器,帮我们在编译的时候生成注解处理器相应的配置文件。

需要注意的是,如果你的注解处理器入口代码是用 Kotlin 写的,那么 AutoService 就傻了。

代码语言:javascript复制
@AutoService(Processor::class)
class AKotlinProcessor: AbstractProcessor() {
    ...
}

为什么呢?显然直接通过上面的这种依赖方式,只会让 Javac 知道有这么个注解处理器,而 Javac 哪里知道还有什么叫 Kotlin 的东西啊,所以我们还得让 kapt 知道才行。

代码语言:javascript复制
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 中)

如何在注解处理器内识别 Kotlin 代码

既然都是 Java 文件,那么我怎么在注解处理器内识别出来哪些代码是 Java 的,哪些是 Kotlin 的呢?其实这个也不难,对比一下就知道了,给大家看一个例子,我有一个 Kotlin 写的类:

代码语言:javascript复制
class Hello {
}

生成的 stub 长这样:

代码语言:javascript复制
@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 编写的,只需要:

代码语言:javascript复制
Metadata metadata = typeElement.getAnnotation(Metadata.class);
//如果有这个注解,说明就是 Kotlin 类。
boolean isKotlin = metadata != null;

一旦能够识别出来注解标注的类是 Kotlin,那么我们就可以采用一些 Kotlin Style 的方式生成代码,例如本来如果是 Java 源码,我会生成这样的一个方法:

代码语言:javascript复制
public class HelloHelper{
    public static void toHelloString(Hello hello){
        ...
    }
}

如果我处理的是 Kotlin 源码,我完全可以生成一个扩展方法让 Kotlin 开发者更愉快地调用:

代码语言:javascript复制
fun Hello.toHelloString(){
    ...
}

当然,这个扩展方法也是可以被 Java 开发者很愉快地调用的。

注意 Kotlin 的类型

我们一再提到注解处理器只认识 Java,所以就算你用 Kotlin 定义了一个方法如下:

代码语言:javascript复制
fun hello(a: Int, b: String){
    ...
}

如果我们用注解处理器处理它的时候,参数 a 的类型就会变成 Java 的 int.class 或者 Integer.class,而参数 b 的类型则会变成 java.lang.String,注意不是 kotlin.String

如果你要根据这些类型对应地去生成代码,你需要将这些类型做映射,例如:

代码语言:javascript复制
java.lang.String -> kotlin.String
java.lang.Integer -> kotlin.Int
int -> kotlin.Int

这个要怎么办呢?不能怎么办,连 J 神的 Kotlin Poet 都没有做这件事儿,如果我们需要写注解处理器生成 Kotlin 的代码,这一点你需要自己来处理。不过呢,我可以给大家一点儿提示,实际上这个类型转换 Kotlin 编译器是做了的,具体可以参考编译器源码:

代码语言:javascript复制
object JavaToKotlinClassMap : PlatformToKotlinClassMap {

    private val javaToKotlin = HashMap<FqNameUnsafe, ClassId>()

    ...    
}

这个 HashMap 当中就存放了需要映射的类型。

怎么生成 Kotlin 源码?

其实我们前面提到了,用 J 神的 Kotlin Poet 这个项目生成 Kotlin 源码的体验几乎与 Java Poet 没差。不过呢,这个项目目前还只是发到了 0.6,所以难免有个小 bug 啥的,例如我要生成一个匿名内部类,就算我只实现了一个接口,它也会给我添加一个构造方法调用的括号:

代码语言:javascript复制
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 才行。

0 人点赞