网上有很多 APT 相关教程,最近开始学这个,发现有一些内容已经过时了,在使用过程中也发现了一些坑,总结一下,形成这篇教程。
本文开发环境:2019年5月初最新版本的 Android Studio 3.4、Android Plugin 3.4.0、Gradle 5.1.1。
本教程需要读者了解注解 Annotation 的基本知识,不涉及 Annotation 运行时反射的用法,专注于自定义 APT 的流程和步骤,以及使用新版 AS 和 gradle 的注意事项。
简介
APT,Annotation Processing Tool,注解处理工具,是 JDK 提供的一个工具。注意是 Java 语言支持的,不是安卓特有的东西,这点对于理解 APT 有一些作用。早期的 JDK 提供了一个单独的 apt 程序,后来被整合到 javac 中了。它最常见的用法就是根据注解自动生成源代码,很多流行的库都使用了注解处理器来生成代码,比如 ButterKnife 会生成资源与变量绑定的代码,让开发者不用手写繁琐重复的 findViewById。
原理
那么 javac 是怎么使用 APT 生成代码的呢?javac 并不知道你想怎么生成代码,需要你按照 javac 提供的规则和接口来自定义 Annotation Processor。注意这是 Java 语言定义的规则。
接口
javax.annotation.processing.AbstractProcessor
,实现这个抽象类,在 process()
方法中自定义生成代码的细节。可以称它为注解处理器 Annotation Processor。只有这一个类型作为接口,当然类中还有一些其他方法用来设置 Annotation Processor 的属性。
规则
- 一个 Annotation Processor 想要参与到 javac 的编译过程中,就要被编译打包成一个 jar 文件。
- 这个 jar 文件要放置在编译期的 classpath 中,javac 会自动查找 classpath 中所有的 Annotation Processor,自动完成注解处理。然而新版的 gradle 5 不再将 Annotation Processor 放在编译期的 classpath 中,导致还需要额外处理,这个是 gradle 的行为,处理方法见下文。
- 这个 jar 文件中包含一个
META-INF/service/javax.annotation.processing.Processor
文件,文件内容是文本,每行一个 Annotation Processor 的完整类名称。
安卓和 gradle
以上是 Java 的基础规则,到了 gradle 中就要按照 Java Plugin 的语法和规则来配置。gradle 一直致力于提高编译速度,在新版的 gradle 5.+ 中,为了推行更快速的增量编译,关闭了一个默认功能,导致由 gradle 4.+ 升级上来的项目有可能构建失败,这其中的弯弯绕绕和坑坑洼洼在下面的步骤中详细讲解。
步骤
1. 项目架构
分 3 个模块:
- annotation 模块:用来定义注解。
- compiler 模块:用来定义 Annotation Processor。
- app 模块:使用注解的应用模块。
为什么要分这么多模块?其中 app 模块是用来测试的,测试新定义的 Annotation Processor 能否成功运行。annotation + compiler 如果写得糙一点可以合并在一起,例如谷歌的 auto service。但两者的目的并不一样,annotation 是专门定义注解的,而 compiler 是处理注解的。最重要的是,annotation(RetentionPolicy.CLASS,RetentionPolicy.RUNTIME)是需要被编译到 app 项目的 class 文件中的,而 compiler 没有必要进入 app 中。
2. annotation 模块
该模块是定义注解用的,可以包含多个注解,例如 ButterKnife 就有二十多个注解定义。而且只有注解的定义,没有其他任何代码。这样做的原因主要是在架构上能单独隔离一个完整内聚的功能,可以被其他模块引用,比如 app 模块必须引用,compiler 模块可以引用。
创建模块
annotation 模块中只有注解定义,可以直接定义为 java library,而不用定义为 android library,定义为安卓库反而会限制它被其他 java library 引用。
build.gradle
apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
然后就可以在这个模块中添加注解定义了。
注解简介
定义注解使用 @interface 关键字,然后使用元注解 @Target
和 @Retention
定义注解的修饰目标和保留策略:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface NiceField {}
-
@Target
指定和约束注解能修饰的代码元素,可以是类、字段、方法、参数等等。 -
@Retention
指定保留策略,指的是被修饰的代码,在编译后是否仍保留注解的策略,有三种:-
RetentionPolicy.SOURCE
:不做任何保留,编译之后就抛弃。 -
RetentionPolicy.CLASS
:保留在 class 文件中,但运行时无法使用。 -
RetentionPolicy.RUNTIME
:保留在 class 文件中,运行时可以通过反射使用。
-
当注解被 Annotation Processor 处理的时候,其实这三种策略的注解都是可以处理的。比如谷歌的 auto service 中的 @AutoService
就是 RetentionPolicy.SOURCE
类型的,用完即抛。如果还有其他运行时处理的需求,可以使用 RetentionPolicy.RUNTIME
。
3. compiler 模块
该模块编译 Annotation Processor 代码,并将其打包成 jar 文件供其他模块使用,一般都起名为 compiler 或 processor。
创建模块
上文说过只要将 jar 放置在了 classpath 中,javac 就会自动查找并执行处理代码。因此只需要创建一个 java library 模块:
build.gradle
apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
implementation 'com.squareup:javapoet:1.10.0' // 使用 javapoet 生成 .java 文件
implementation project(':annotation') // 依赖 annotation 模块方便引用其中的注解
compileOnly 'com.google.auto.service:auto-service:1.0-rc5'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5'
}
最后两行的依赖是谷歌的 auto service。为啥两行后面是一样的?
谷歌 auto service 和 META-INF
谷歌的 auto service 也是一种 Annotation Processor,它能自动生成 META-INF 目录以及相关文件,避免手工创建该文件,手工创建有可能失误(我就写错过路径)。使用 auto service 中的 @AutoService(Processor.class)
注解修饰 Annotation Processor 类就可以在编译过程中自动生成文件。
可见 auto service 是给 Annotation Processor 服务的 Annotation Processor,是不是很有趣。可能有人要问 auto service 的 META-INF 目录和文件怎么办?不是还可以手工写嘛。
手工怎么写这个 META-INF? 在 jar 包中路径是
META-INF/service/javax.annotation.processing.Processor
,见下图:
但在项目中应该放在哪里呢?见下图:
手工生成这个 META-INF 目录和文件就不用引入 auto service 依赖了。
如果引入的话,还要注意有两个配置 compileOnly
和 annotationProcessor
。
compileOnly 'com.google.auto.service:auto-service:1.0-rc5'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5'
这两个重复写的原因就是 auto service 并没有将 annotation 和 processor 分割成两个项目,而是混到了一起。
-
compileOnly
所需的只是@AutoService
这个注解。表示只参与编译过程并不打包到最终产物 jar 文件中,而注解@AutoService
的 RetentionPolicy 就是 SOURCE,不会编译生成到 class 文件中。即便使用compile
依赖,最终生成的 jar 包中只会多了 auto service 提供的注解,其他 class 文件部分不受影响,因此可以使用compileOnly
只参与编译过程。 -
annotationProcessor
这个是新版 gradle 提供的 java plugin 内置的配置项,代替了早期第三方提供的android-apt
插件。而且,敲重点,在 gradle 5.+ 中将 Annotation Processor 从编译期 classpath 中去除了,javac 也就无法发现 Annotation Processor。此处如果按照 gradle 4.+ 的写法,只写一个compileOnly
是无法使用 auto service 的 Annotation Processor 的。必须要使用annotationProcessor
来配置 Annotation Processor 使其生效。
定制化 Processor
两个部分:
- 定义文件生成规则
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 因为运行在 javac 执行过程中,打印必须使用 messager,而不能使用 System.out
messager.printMessage(Diagnostic.Kind.NOTE, "processing");
// 此处正经工具应该使用参数 set 和 roundEnvironment 根据注解的具体使用情况来生成代码
// 本文主要讲步骤和配置,不涉及这个部分。仅生成了一个独立的 java 文件。
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.study.app", helloWorld)
.build();
try {
// 最后要将内容写入到 java 文件中,这里必须使用 processingEnv 中获取的 Filer 对象
// 它会自动处理路径问题,我们只需要定义好包名类名和文件内容即可。
Filer filer = processingEnv.getFiler();
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
// 返回值表示处理了 set 参数中包含的所有注解,不会再将这些注解移交给编译流程中的
// 其他 Annotation Processor。一般都不会有多个 Annotation Processor,一般都写 true。
return true;
}
- 配置 Processor。有两种配置方法:可以用注解的方式也可以重写
AbstractProcessor
的某些方法。如果两种方式都定义,则会使用方法的版本,但从工程的角度应该只用一种方法来设置,以免混淆。这些注解设置为了 RetentionPolicy.RUNTIME 类型,如果不重写相关方法,AbstractProcessor
中方法的默认实现则会使用反射来获取注解中设置的值。
// 注解方式
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.study.annotation.NiceField")
public class MyProcessor extends AbstractProcessor {
// 重写方法方式
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(NiceField.class.getCanonicalName());
}
}
- SupportedSourceVersion 指定最低支持的源代码版本,这是指 javac 的版本,对安卓来说只有 7 和 8 的区别,直接使用当前编译环境最高支持版本即可:
SourceVersion.latestSupported();
- SupportedAnnotationTypes 指定该 Annotation Processor 可以处理哪些注解,这里要返回一个字符串的集合,字符串内容是这些注解的完整类路径,即
class.getCanonicalName()
。
4. app 模块
该模块用来测试和验证 Annotation Processor,是一个简单的 android application 模块。
创建模块
就不贴图了,默认一个 android 项目就会有一个 app 模块,看下面的 build.gradle:
// 前半部分都是自动生成的不用细看
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.study.app"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
// 上面部分都是自动生成的不用细看
dependencies {
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// 使用 annotationProcessor 来指定作为 Annotation Processor 的模块
annotationProcessor project(':compiler')
// 引入自定义的注解
implementation project(':annotation')
}
使用注解
package com.ajeyone.study.aptda;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.ajeyone.study.annotation.NiceField;
public class MainActivity extends AppCompatActivity {
@NiceField // 在这里
int currentValue;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
currentValue = currentValue + 1;
}
}
构建后生成了 HelloWorld.java:
总结
本文介绍了 APT 的原理以及自定义 APT 的流程和步骤。在研究过程中也发现了一些坑:
- gradle 脚本中要使用
annotationProcessor
来指定 Annotation Processor 库。 - 谷歌 auto service 没有分离 annotation 和 processor,在新版的 gradle 脚本中需要用
annotationProcessor
将其指定为 Annotation Processor 才会起作用。
关于 APT 中另一个块重要的内容,如何通过注解获取源代码的信息,并生成目标源文件,建议阅读一些流行开源项目的源代码,比如 ButterKnife,Dagger2 等等。
另外还有一个比较新的东西是 incremental annotation processor,这个是 gradle 4.7 就搞出来的加快编译速度的功能,跟 java 本身没有关系,但是要引入 gradle 提供的一些工具来修改 Annotation Processor,感兴趣的可以参考一下官网的资料以及一些开源项目的实现,比如 Dagger2。https://docs.gradle.org/4.7/userguide/java_plugin.html#sec:incremental_annotation_processing