增量APT

增量APT

背景

这是一个Gradle提供的特性,其在4.7版本后提供了对增量处理注解的支持。这个功能依赖Java的增量编译,否则也是不生效的。

正文

Gradle通过“类别”Category来识别可以进行增量工作的注解处理器,可以支持两种类型:

  1. isolating 隔离
  2. aggregating 聚合

从注册说起

正统的APT注册方式是在META-INF文件夹(src/main/resources/META-INF)下注册的:

services目录下,创建javax.annotation.processing.Processor文本文件,其中写入自己Processor类的全称

这是大家都知道的事情。Google的auto这个脚手架自动帮我们完成了这一点。

但是对于需要支持增量工作的注解处理器来说,原有的注册方式要改动一下了:

  • META-INF目录不变,services目录改为gradle,即META-INF/gradle/
  • gradle目录下创建的文本文件名为:incremental.annotation.processors

其中填写的文本内容格式为:

1
[注解处理器的类全称],[类型]

中间必须是半角的逗号

而类型这个数据在上边提到了两个,其实还有一个,是:dynamic

类型选择

如果你的处理器只能在运行时才能判断自己可不可以支持增量,那么就选择dynamic,然后在运行时,通过Processor类的getSupportedOptions()接口进行返回,例如:

1
2
3
4
@Override
public Set<String> getSupportedOptions() {
return Collections.singleton("org.gradle.annotation.processing.aggregating");
}

这样和在配置文件中写入aggregating是一个效果。(注意,这里返回的是数组,其中的字符串内容是包含了固定的gradle的包名内容)

需要遵守的规则

想要让你的注解处理器支持增量的话,无非是两种类型。这个在上边说过了,而这两种类型,都会有一些限制,开发者必须遵守这些限制,那么才能顺利的支持增量编译:

  1. 必须只能使用FilerAPI进行文件的生成,其他的方式可能会导致失败(如官方所言,是无声的失败),或者生成的文件不能被正确清除,可能污染最终的生成物

  2. 众所周知,在APT环节中有时为了直接操作、甚至干涉AST的数据结构,我们可能直接转化为其真正的实现者,即:JavacProcessingEnvironment,进而去操作JCTree等数据结构。但在实现上,Gradle是封装了一层Processor的API的,所以这样的强转行为可能导致失败,也就导致这个注解处理器不能支持增量编译。所以,需要开发者使用正统的接口API进行操作,而不要强转

  3. 对于资源的生成,只支持FilerAPI是自然,对于createResource()方法中的location参数来说,只能支持StandardLocation中的:

    1. CLASS_OUTPUT
    2. SOURCE_OUTPUT
    3. NATIVE_HEADER_OUTPUT

    其他的参数在增量编译中不支持

isolating

最快的类型,这个类型把每一个注解节点视作一个独立的个体,他们互相不产生影响,每一个来了都能单独进行处理或者校验。它的限制是:

  1. 不能依赖当前RoundEnvironment中的其他Element(非此Annotation的)做决策。所谓增量,就是每次编译的文件不是全量的,只是少部分,所以这里的输入是少的,只是必要的那些才会给你。如果开发者的处理器真的需要其他不相关的节点的话,那么需要声明成为agregating聚合类型
  2. FilerAPI的createXXX接口会需要传入原始类型,JavaPoet项目中的TypeSpec.Builder也有相关API。对于这个接口来说,此类型只能提供一个参数,也就是只能而且必须只有一个原始类型。0或者更多都会导致重新编译所有源文件

aggregating

如果需要聚合多个源码文件才能生成一个或多个的文件或者做校验工作,那么这种处理器就是聚合类型了。官方给出的例子是ServiceRegistry,这种场景下需要遍历多个文件然后最终产出一份注册表。结合此类型的限制,可以更好的理解:

  1. 这种类型只能读取CLASS或者RUNTIME的注解
  2. 对于参数的输入只能通过开发者传递-parameters编译参数

结合以上,笔者认为,“聚合”类型的处理器是有可能接收到已经在上一次编译成为class文件的源码的(那么相对来说,isolating就不能接收到class文件,只会收到java源文件)。所谓“聚合”,就是结合之前可能存在的几轮APT操作后,混合着class和Java文件再作为后续的输入进行数据处理。所以,聚合类型有较大的资源获取权限来进行更大范围的处理。

总结

虽然结合了大量参考资料的阅读和亲身测试,但笔者对“聚合”和“隔离”两个类型还是不能给出十分明确的代码层面的区分,这一点恐怕要在多处理器的环境下方可验证。后续如果有最新的进展我也会更新到此处。

对于已有处理器,如何增加对增量编译的支持,这里简单做一个总结:

  1. 首先,Gradle版本要在4.7之上
  2. APT支持增量的基础是Java的编译是支持的
  3. 增量处理器要在META-INF下通过新的注册文件告知Gradle构建系统
  4. 开发者需要按需扩大注解(@annotation)的生效范围,对于聚合来说,SOURCE就不够用了
  5. 必须规范对AST的操作,实际上也就只能用只读的方式读取AST了,也就是用原生的接口API,否则将无法支持增量编译
  6. 资源、源文件的生成只能通过Filer接口进行

参考