美文网首页
Java Annotation 高级实践

Java Annotation 高级实践

作者: 你可记得叫安可 | 来源:发表于2019-06-01 23:25 被阅读0次

一些概念厘清

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 是怎么运行的?

  1. apt 扫描所有的源码,从中找出所有的注解 → 这些注解称为 未申明的注解
  2. apt 找到所有的 processor
  • 被标记了 @AutoService(Processor.class)processor
  • resources/META-INF/services/javax.annotation.processing.Processor 中配置的注解器
  1. apt 遍历每一个注册的 processor,从 AbstractProcessor.getSupportedAnnotationTypes() 中获取每个 processor 所关心的注解类型 → 这些注解称为申明的注解
  2. 所有当所有申明的注解覆盖了所有未申明的注解,apt 停止遍历
  3. apt 依次调用这些 processor.process(),并传入它们自己申明的注解。如果调用完成后,有新的文件被生成,那么 apt 会再遍历一次 processor 集合。当没有新文件生成时, apt 会再调用最后一次 processor.process(),执行最后一次 round。因此一个会产生新文件,且新文件中没有使用 processor 所关心注解时,这个自定义 processor 会被调用 3 次。
  4. 当没有新的文件生成时,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. 它自身开发者很少用,但是 ExecutableElementTypeElement 都继承于它。Parameterizable 提供方法 List<? extends TypeParameterElement> getTypeParameters(); 用于返回被类或方法申明的类型参数(就是我们看到的 <T extend XXX> 这样的)
QualifiedNameable A mixin interface for an element that has a qualified name. 它自身开发者很少用,但是 PakcageElementTypeElement 都继承于它。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,它对应的 AnnotationValueRetentionPolicy.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 的应用,表示 Name 名称替换。
  • $T,表示 Type 类型替换。("$T foo", List.class)List foo,好处是 JavaPoet 会自动帮助你 import 被替换的类。
  • $L,表示 Literal 字面量替换。("abc$L123", "FOO")abcFOO123
  • $S,表示 String 字符串替换。("$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 中。

这是基于上述思路的代码

相关文章

网友评论

      本文标题:Java Annotation 高级实践

      本文链接:https://www.haomeiwen.com/subject/lekazqtx.html