一些概念厘清
APT(Annotation Processor Tool) 向已有的 Java 文件添加文件吗?
不能。APT 只能生成新的文件(代码文件或其他文件),不能向已有文件添加新的内容。
Annotation 如何传参?
// Annotation 定义
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnLifecycleEvent {
LifecycleEvent value();
}
// 带默认参数的 Annotation 定义
@Retention(RetentionPolicy.RUNTIME)
@Target()
// 如下使用, ON_APP_START 是一个 LifecycleEvent 类型的枚举
public class XXX {
@OnLifecycleEvent(value = LifecycleEvent.ON_APP_START)
public void onAppStart() {}
}
AbstractProcessor 运行在哪里?
- 继承自
AbstractProcessor
的类是真正处理 Annotation 和生成代码的地方。这部分代码是运行在一个独立的 JVM 中的。它不会参与最终 app 的编译和打包,因此可以 implementation 任意三方工程。 - 也是因为
AbstractProcessor
是运行在一个独立的 JVM 中,因此我们调试注解器需要使用远程调试。
AbstractProcessor 的 process 方法什么时候被调用?
这个问题的本质其实是,apt 是怎么运行的?
- apt 扫描所有的源码,从中找出所有的注解 → 这些注解称为 未申明的注解。
- apt 找到所有的
processor
- 被标记了
@AutoService(Processor.class)
的processor
类 - resources/META-INF/services/javax.annotation.processing.Processor 中配置的注解器
- apt 遍历每一个注册的
processor
,从AbstractProcessor.getSupportedAnnotationTypes()
中获取每个processor
所关心的注解类型 → 这些注解称为申明的注解。 - 所有当所有申明的注解覆盖了所有未申明的注解,apt 停止遍历
- apt 依次调用这些
processor.process()
,并传入它们自己申明的注解。如果调用完成后,有新的文件被生成,那么 apt 会再遍历一次processor
集合。当没有新文件生成时, apt 会再调用最后一次processor.process()
,执行最后一次 round。因此一个会产生新文件,且新文件中没有使用processor
所关心注解时,这个自定义processor
会被调用 3 次。 - 当没有新的文件生成时,apt 调用 javac 开始编译原有的和新生成的文件。
既然 process()
可能会被调用多次,那么每个 processor 如何知道当前 round 是最后一次呢(方便进行类似关闭文件的操作)?
RoundEnvironment.processingOver
RoundEnvironment 究竟包含哪些东西?
它是这次 round 的环境,主要包括一堆 Element
方法 | 说明 |
---|---|
getRootElement |
包括所有的 Element ,不止该 processor 所关心的 |
getElementsAnnotatedWith |
返回被特定注解标记的 Element
|
什么是 Element?
Element
表示元素,位于包 javax.lang.model.element 中。Element
是语言级别的一种抽象,有点像建模语言(UML)上的概念,是程序中的一种元素,具体到 Java 中就是一个类(class)、一个方法(method)、一个变量(field),在其他语言中又是另外的东西(python 中 class, define 的方法,任意的变量 等)。它在 java 中的实现就用一个接口来表示。我们看看继承于它的 javax.lang.model.element 中的其他接口。
接口 | 说明 |
---|---|
ExecutableElement |
Represents a method, constructor, or initializer (static or instance) of a class or interface, including annotation type elements. 即 类或接口(包括注解接口 → @interface )的方法、构造函数、初始化器 |
PackageElement |
Represents a package program element. Provides access to information about the package and its members. 即包相关信息,包括包内的类、接口等 |
Parameterizable |
A mixin interface for an element that has type parameters. 它自身开发者很少用,但是 ExecutableElement 和 TypeElement 都继承于它。Parameterizable 提供方法 List<? extends TypeParameterElement> getTypeParameters(); 用于返回被类或方法申明的类型参数(就是我们看到的 <T extend XXX> 这样的) |
QualifiedNameable |
A mixin interface for an element that has a qualified name. 它自身开发者很少用,但是 PakcageElement 和 TypeElement 都继承于它。QualifiedNameable 提供方法 Name getQualifiedName(); 用于返回包、类的全限定名 |
TypeElement |
Represents a class or interface program element. Provides access to information about the type and its members. Note that an enum type is a kind of class and an annotation type is a kind of interface. 即代表类、接口、枚举、注解 |
TypeParameterElement |
Represents a formal type parameter of a generic class, interface, method, or constructor element. 即类型参数 |
什么是 AnnotationMirror?
我们自定义 annotation 时常见的@Retention(RetentionPolicy.RUNTIME)
就是一个 AnnotationMirror
。
- 它通过方法
getAnnotationType
来获得具体注解的DeclaredType
申明类型。这个注解的申明类型就是 java.lang.annotation.Retention - 它通过方法
getElementValues
来获得一个 ExecutableElement - AnnotationValue 列表。其实这个注解有个隐藏 ExecutableElement:@Retention(value = RetentionPolicy.RUNTIME)
,即 value,它对应的 AnnotationValue 是RetentionPolicy.RUNTIME
。
来看一个例子
public class ProcessingUtils {
private ProcessingUtils(){}
// 根据传入的 annotation 集合,找到它们所在类的 TypeElement
public static Set<TypeElement> getTypeElementsToProcess(Set<? extends Element> elements,
Set<? extends Element> supportedAnnotations) {
Set<TypeElement> typeElements = new HashSet<>();
for (Element element : elements) {
if (element instanceof TypeElement) {
boolean found = false;
for (Element subElement : element.getEnclosedElements()) {
for (AnnotationMirror mirror : subElement.getAnnotationMirrors()) {
for (Element annotation : supportedAnnotations) {
if (mirror.getAnnotationType().asElement().equals(annotation)) {
typeElements.add((TypeElement) element);
found = true;
break;
}
}
if (found) {
break;
}
}
if (found) {
break;
}
}
}
}
return typeElements;
}
}
JavaPoet 释疑
FieldSpec clazz = FieldSpec.builder(Class.class, "clazz").build();
MethodSpec constructor = MethodSpec.constructorBuilder().addParameter(Class.class, "clazz")
.addStatement("this.$N = clazz", clazz)
.build();
上面的 JavaPoet 代码将构造出以下 Java 代码:
public Class clazz;
public ObserverHolder(Class clazz) {
this.clazz = clazz;
}
- 注意其中对
$N
的应用,表示 名称替换。 -
$T
,表示 类型替换。("$T foo", List.class)
→List foo
,好处是 JavaPoet 会自动帮助你 import 被替换的类。 -
$L
,表示 字面量替换。("abc$L123", "FOO")
→abcFOO123
-
$S
,表示 字符串替换。("$S.length()", "foo")
→"foo".length()
。$S
替换为带双引号的字符串。
更详细的用法可以参考 javapoet基础用法,或者下面的实践
一个典型自定义 Annotation 的写法
在公司的项目实践中有这样的需求:app 需要与另一个非手机设备连接后才能使用,app 中有许多逻辑是设备连接上后就需要立即执行,设备断链后就停止执行,回收资源。或者有的逻辑比较关心某个特定 Activity
的进入和退出事件。我们称这样的逻辑为 逻辑孤岛(比如读取设备的 SN 号),即它不依赖于其他业务模块,只需要在一个事件起来,在另一个事件结束。
- 我们初步的模型可以是有一个管理类
LifecycleManager
之类的,它负责监听设备连接事件,并且在连上 / 进入特定 Activity 时创建 逻辑孤岛 对象,在断链 / 退出特定 Activity 时销毁 逻辑孤岛 对象。这个LifecycleManager
类是典型的模板类,里面有许多重复的、体力活的代码。而注解生成代码文件正式帮助开发者减少写这些模板类的工作量。 - 这种事件通知类型的需求,又让我想到了
Eventbus
的设计。因此我们可以参考Eventbus
的设计,在代码编译阶段生成一个 Index 文件,它的主要作用就是用于存储 类 -- 事件 -- 方法 的一个映射表。同时又比 Eventbus 的索引文件多了一个 逻辑孤岛 对象生命周期管理的机制。
在实践中,我总结出自定义注解的代码大体上可以分为 3 个 module: - Processor module。我们的自定义
processor
就放在这个 module。它运行在另一个 JVM,用于生成新的类文件。新生成的类文件最好实现一个对外暴露的 annotation interface,这样方便下面的 business module 在新类文件未生成时使用(编码阶段)。 - Annotation module,主要放自定义注解类,以及一些其他会被 processor module 生成的新文件所用到的中间类和 annotation interface,因为 processor module 最好只依赖 annotation module。
- Business module,其中的类会被 app 业务直接用到,同时它会使用 annotation interface,以在新类文件未生成时,开发者能够完成编码。
实践中,不要一开始就凭空想象这个新类文件的模样,我们最好先将新生成类文件用实际代码写一份,确保逻辑正确能跑通,然后再写自定义 processor
去生成这份类文件代码。由于 processor
的维护成本可能会高一点,因此新类文件最好能逻辑越少越好,将其余逻辑放在 business module 中。
网友评论