Android-编译时注解APT

Android-APT

APT (Annotation Processing Tool) 发生在 Javac 编译成 Class 文件之后、Class 编码成 Dex 之前。Processor 处理的输入数据是 Javac 编译后的 Class 信息,因此 APT 的执行流程在 Javac 之后,对于 Android 开发中多 Module 的场景,由于每一个 Module 都是并行编译的,只有在产生依赖关系时才会具有先后顺序,因此只要 Module 在 Gradle 中声明了 annotationProcessor,则每个声明了的 Module 都会独立在 Javac 之后调用对应的 Processor 进行处理。因此 Processor 的代码可能会被调用多次,而且每一次调用时的环境都是独立、互不影响的。

1. 自定义APT

创建自定义的 Annotation Processor 有两个前提:

  • Processor Module 必须是纯 Java / Kotlin Library
  • Module 只需要通过 annotationProcessor xxx 即可应用 APT,不需要也不能依赖 Processor Module

因此 APT 的整体结构一般分为如下几个部分:

  • AnnotationModule:用于存放自定义注解类,并且为纯 Java / Kotlin Library(应为需要被 Processor Module 依赖,而后者也是纯 Java / Kotlin Library);
  • ProcessorModule:用于存放扫描自定义注解的 Processor,依赖 AnnotationModule,同样为纯 Java / Kotlin Library;
  • APIModule:用于对外提供业务功能,因为 APT 的作用是根据注解完成一些自动化逻辑,因此通常还会提供一些接口,当然如果是一个纯粹服务于编译流程的 APT,可以没有该 Module。同样依赖 AnnotationModule,可以是纯 Java / Kotlin Library、也可以是一个 Android Library,。

1.1 创建注解类

1
2
3
4
5
6
7
8
9
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DemoAnnotation {
// 示例:如何为注解添加参数
// 如果参数名为 value,则使用注解时可以隐式赋值
int value() default 0;
}

1.2 创建Processor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// @SupportedOptions 注解搭配 #getSupportedOptions() 回调使用
@SupportedOptions({"org.gradle.annotation.processing.aggregating"})
public class DemoProcessor extends AbstractProcessor {

private Filer filer;
private Messager messager;
private Types typeUtils;
private Elements elements;

/**
* 初始化 Processor 环境,通常用于初始化 Filer,Message,ElementUtil。
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);

filer = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
typeUtils = processingEnvironment.getTypeUtils();
elements = processingEnvironment.getElementUtils();
}

/**
* 支持的 JDK 版本,通常选择最新版。
* 如果用到了某个版本 JDK 特性,则需要选择该版本或更新版本。
*/
@Override
public SourceVersion getSupportedSourceVersion() {
// return SourceVersion.RELEASE_8;
return SourceVersion.latestSupported();
}

/**
* 指定 Processor 编译选项,需要 Processor 添加 @SupportedOptions 并指定同样的 Options。
* <p/>
* 通过该方法可以指定 Processor 的编译方式,包括全量、增量两种:
* <ul>
* <li>默认为全量编译。</li>
* <li>在 META-INF 中注册 Processor 时指定为 isolating,则使用增量编译。</li>
* <li>在 META-INF 中注册 Processor 时指定为 dynamic,则可在该方法中选择增量编译。</li>
* </ul>
* Note: 如果一个 Module 应用了 Processor 但又没有任何一个类被 Process、
* 或是在 Gradle 中设置了自定义 compileOptions 但未在 SupportedOptions 中定义,
* 则会出现日志警告:
* <pre>
* The following options were not recognized by any processor: '[xxxOptionName]'
* </pre>
* Note: Auto-Service 1.0-rc6 才支持增量编译。
*/
@Override
public Set<String> getSupportedOptions() {
// return super.getSupportedOptions();
// 支持增量模式:
return Collections.singleton("org.gradle.annotation.processing.aggregating");
}

/**
* 用于指定该 Processor 需要处理的注解的类。
*
* 一个 Processor 可以同时处理多个注解,所以返回值为 Set。
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return Colletions.singleton(DemoAnnotation.class.getCanonicalName());
}

/**
* 编译时处理注解的入口。
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}

1.3 注册Processor

在 Android 中使用自定义注解,有两种注册 Processor 的方式:

  • AutoService 自动注册
  • META-INF 中手动注册

假设 Processor 所在 Module 为 module_processor

1.3.1 AutoService自动注册

借助 Google 推出的 AutoService 工具自动完成 Processor 的注册:

1
2
3
4
5
6
7
8
9
// Processor 所在 Module 添加 AutoService 的依赖,并用 AutoService 的注解处理器代替完成注册:
implementation 'com.google.auto.service:auto-service:1.0'
annotationProcessor 'com.google.auto.service:auto-service:1.0'

// 给 Processor 添加 AutoService 的注解:
@AutoService(Processor.class)
public class DemoProcessor extends AbstractProcessor {
......
}

实际上就是用 @AutoService 的 Processor 动态创建了 DemoProcessor 的注册信息完成注册的。

1.3.2 META-INF中手动注册

通过 META-INF 手动注册,与 Java 中的注册方式是一样的:

(1)在 Processor 所在 Module 下创建注册信息文件:

1
2
3
4
5
6
7
8
9
10
11
module_processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor

// 该文件目录结构如下:
module_processor
└─ src
└─ main
└─ java
└─ resources
└─ META-INF
└─ services
└─ javax.annotation.processing.Processor

(2)在 javax.annotation.processing.Processor 中声明 Processor 的全路径,例如:

1
priv.demo.DemoProcessor

重新 Build 即可发现 DemoProcessor 已经可以正确处理。

1.4 使用Processor

当一个 Processor 成功被注册后,默认情况下只会处理 Processor 所在 Module 的注解,如果其他 Module 也需要使用该 Processor,则需要在 Gradle 中声明:

1
2
// 应用 module_processor 的 Processor,并不要求与 module_processor 有依赖关系
annotationProcessor project(path: ':module_processor')

如果想对 Gradle 编译时断点调试,需要新增单独的调试任务:

(1)在 Run/Debug Configurations 中新增一个 RemoteRemote JVM Debug 任务,然后再右侧栏将目标地址选择为本地的某个端口,例如 8888:

1
2
3
Name: RemoteDebug(可自定义)
......
Host: localhost Port: 8888

(2)修改项目根目录下的 gradle.properties,为 Gradle 添加 2 条 Debug 配置:

1
2
3
4
-Dorg.gradle.debug=true

# 注意配置中 address 参数的端口号要和 Configuration 中的一致:
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888

(3)重新进入 Run/Debug Configurations 中查看新增的 Remote Configuration,可以在其中的 Command line arguments for remote JVM 看到新增的 Gradle 配置参数。

(4)在 Run/Debug Configurations 中选中新增的 Remote Configuration,执行 Debug,然后再执行编译任务(Rebuild, Assemble...),即可进入断点。


2. Processor开启增量编译

通过上述方式成功注册一个 Processor 后,在 Build 时 Gradle 可能会报如下警告:

1
2
The following annotation processors are not incremental: jetified-module_processor.jar (project :module_processor).
Make sure all annotation processors are incremental to improve your build speed.

大致含义为 Processor 每次都是全量编译的,尽可能确保采用增量编译以提高编译速度。

查阅相关资料后发现,增量编译选项需要在 Processor 的注册信息中指定,对于上文中的两种注册方式:

  • 如果使用 AutoService 自动注册,Auto-Service 在 1.0-rc6 版本中才支持增量编译。
  • 如果使用手动注册,则需要从 Java 原生模式注册信息修改为 Gradle 注册信息。

2.1 AutoService开启增量编译

将 AutoService 的依赖以及对应的 annotationProcessor 升级到 1.0-rc6 或以上即可。

2.2 手动注册开启增量编译

手动注册的 Processor 要开启增量编译,则需要切换为 Gradle 模式的注册信息。

(1)在 Processor 所在 Module 创建 Gradle 模式的注册信息文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
module_processor/src/main/resources/META-INF/gradle/incremental.annotation.processors

// Java 原生模式和 Gradle 模式的注册信息文件结构对比:
module_processor
└─ src
└─ main
└─ java
└─ resources
└─ META-INF
└─ gradle
└─ incremental.annotation.processors (Gradle 模式注册信息文件)
└─ services
└─ javax.annotation.processing.Processor (Java 原生模式注册信息文件)

两种模式的注册信息文件可以共存,Gradle 编译时会优先选择 Gradle 模式的注册信息。

(2)在 incremental.annotation.processors 中声明支持增量编译的注册信息:

1
priv.demo.DemoProcessor,isolating

重新 Build,Gradle 相关警告日志已经消除。

此外,Gradle 模式注册信息还支持在 Processor 代码中决定编译类型,只需要在 incremental.annotation.processors 中的注册信息声明为 dynamic 类型,然后在 Processor#getSupportedOptions(...) 中指定具体的编译类型即可:

1
2
3
4
5
6
7
8
9
10
11
12
// incremental.annotation.processors 文件中声明为 dynamic 模式:
priv.demo.DemoProcessor,dynamic

// Processor#getSupportedOptions(...) 中指定编译类型:
public class DemoProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedOptions() {
// Aggregating 增量编译
return Collections.singleton("org.gradle.annotation.processing.aggregating");
}
......
}

3. APT过滤元素

一个 Processor 可以同时处理多个自定义注解,因此过滤 Class 分为两步:

  • 确定需要过滤的注解
  • 获取并处理包含注解的元素(类、方法、属性等)。

3.1 选择需要过滤的注解

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DemoProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> routerAnnotationTypeSet = new HashSet<>();
// 添加需要处理的注解类的全限定类名。
routerAnnotationTypeSet.add(DemoAnnotation.class.getCanonicalName());
// 值得注意的是,方法返回的集合是注解类全限定类名的集合,
// 这意味着 Processor 并不一定要真正具有注解类的依赖关系,
// 手动指定一个类的全限定类名也是可行的,例如:
routerAnnotationTypeSet.add("priv.demo.DemoAnnotation");
return routerAnnotationTypeSet;
}
}

3.2 根据注解过滤需要处理的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class DemoProcessor extends AbstractProcessor {
private Elements elementUtils;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);

elementUtils = processingEnvironment.getElementUtils();
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set == null || set.isEmpty() || roundEnvironment == null) {
return false;
}

if (roundEnvironment != null && roundEnvironment.processingOver()) {
// 示例:判断是否正在处理当前 Module 的最后一个
}

// 获取包含了某个注解的元素集合:
Set<? extends Element> demoAnnotatedClassSet = roundEnvironment.getElementsAnnotatedWith(DemoAnnotation.class);

// 同样也可以直接通过注解的全限定类名获取:
TypeElement demoAnnotationClassElement = elementUtils.getTypeElement("priv.demo.DemoAnnotation");
Set<? extends Element> demoAnnotatedClassSet = roundEnvironment.getElementsAnnotatedWith(demoAnnotationClassElement);

// 处理包含了注解的元素:
for (Element annotatedElement : demoAnnotatedClassSet) {
processElement(annotatedElement);
}
return true;
}

private void processElement(RoundEnvironment roundEnvironment, Element annotatedElement) {
if (annotatedElement == null) {
return;
}

if ((annotatedElement instanceof TypeElement) && annotatedElement.getKind() == ElementKind.CLASS) {
// 示例:判断注解是否声明在类上,同理也可判断是否在方法、属性等元素上,
// 可根据注解定义的类型选择是否需要处理该元素。
}

if (annotatedElement.getModifiers().contains(Modifier.PUBLIC)) {
// 示例:判断被注解的元素包含的标识符,同理也可判断 private、static 等。
}

// 示例:获取被注解的 Class(前提是被注解的元素是 Class 类型)实现的所有接口:
TypeElement annotatedTypeElement = ((TypeElement) annotatedElement);
for (TypeMirror eachTypeMirror : annotatedTypeElement.getInterfaces()) {
// 示例:判断某个接口是否是另一个接口或是其子类:
TypeMirror superInterfaceTypeMirror = elementUtils.getTypeElement("priv.demo.IDemoInterface").asType();
boolean isSubInterface = typeUtils.isSubtype(eachTypeMirror, superInterfaceTypeMirror);
}
}
}

需要注意的是,当使用全限定类名的方式(elementUtils.getTypeElement("priv.demo.DemoAnnotation"))获取注解类对应的 TypeElement 对象时,仅当应用了 Processor 的 Module 可以访问注解类时返回值才不为 null,但与 Processor 所在 Module 本身是否可以访问注解类无关,例如:

1
2
3
- module_annotation:不依赖任何 Module
- module_processor:不依赖任何 Module
- module_app:通过 annotationProcessor project(path: ':module_processor') 应用 APT

此时如果 module_app 具有 module_annotation 的依赖,则 elementUtils.getTypeElement("priv.demo.DemoAnnotation") 可以正确返回 DemoAnnotation 的 TypeElement 对象,否则将返回 null,与 module_processor 是否具有 module_annotation 的依赖无关。


4. APT生成类

成功注册一个 Processor 后,就能通过对应的注解获取相关信息,并根据这些信息动态生成类。生成类文件时,可以通过 JavaPoet 等工具生成,也可以自行拼接代码文本 String 内容后直接写入文件。

4.1 在同一个Module中生成类

仍以 DemoAnnotation 为例:

1
2
3
4
5
6
7
8
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DemoAnnotation {
// 如果参数名为 value,则使用注解时可以隐式赋值
int value() default 0;
}

以 JavaPoet 为例,获取所有添加了 @DemoAnnotation 注解的类,并在与之相同的 Module 的 Build 目录下生成一个名为 {ClassName}_{AnnotationValue} 新的类:

例如:

  • @DemoAnnotation class DemoClassA {} 将生成一个名为 DemoClassA_0 的类(因为 DemoAnnotation#value() 的默认值为 0);
  • @DemoAnnotation(123) class DemoClassB {} 将生成一个名为 DemoClassB_123 的类。

使用 JavaPoet 需要添加依赖:implementation 'com.squareup:javapoet:1.13.0'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class DemoProcessor extends BaseRouterProcessor {

private void processElement(Element annotatedElement) {
// 示例 1:获取被添加了注解的类所在的包名以及类名
if ((eachElement instanceof TypeElement) && annotatedElement.getKind() == ElementKind.CLASS) {
String packageName = annotatedElement.getEnclosingElement().toString();
String className = annotatedElement.getSimpleName().toString();
}

// 示例 2:获取元素上的注解实例(前提是有注解类的依赖,能直接访问注解类),返回值是泛型:
DemoAnnotation demoAnnotation = annotatedElement.getAnnotation(DemoAnnotation.class);
// 然后就能获取使用注解时的参数赋值(如果有参数的话):
int value = demoAnnotation.value();

// 示例 3:如果 Processor 不能直接访问注解类,则只能获取注解包含的抽象参数 List:
// 可以通过 List 是否为空判断元素上的注解是否设置了参数,
// 如果参数是枚举或 String 类型,可以尝试是否能通过字符串匹配的方式判断取值。
List<? extends AnnotationMirror> annotationMirrorList = element.getAnnotationMirrors();

// 示例:使用 JavaPoet 生成类(伪代码)
try {
JavaFile.builder(packageName,
TypeSpec.classBuilder(className + "_" + value)
.addJavadoc("DO NOT edit this file !!!")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.build())
.build()
.writeTo(filer);
messager.printMessage(Kind.NOTE, "Generated: " + packageName + "." + className);
} catch (Exception e) {
messager.printMessage(Kind.ERROR, "Failed: " + packageName + "." + className);
throw new RuntimeException(e);
}
}
}

其中在 Processor#process(...) 中,通过 JavaPoet 生成类的最后一步,调用的是 writeTo(filer),而 filer 是在 Processor#init(...) 中通过运行环境获取的,可以理解为:每一个 Module 在 Javac 之后,Processor 都会被当前 Module 调用,因此 Filer 中的信息都是针对当前 Module 的,所以 writeTo(filer) 将会在当前 Module 下生成文件。

4.2 在任意目录生成类

通过查看 JavaPoet 和 Filer 的源码可以知道,writeTo(filer) 实际上也是通过 Filer 获取了当前 Module 的路径信息,只不过该路径信息是相对当前 Module 的,但实际上 JavaPoet 的 writeTo(...) 是允许直接指定任意路径的,只需要传入对应路径的 Path 对象即可:

1
2
3
4
5
6
File generatedFile = new File("/DemoProject/" + className + ".java");
JavaFile.builder(packageName,
TypeSpec.classBuilder(className)
.build())
.build()
.writeTo(generatedFile.toPath());

参考文献