Java Annotation 高级实践

2019-06-01  本文已影响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 的 process 方法什么时候被调用?

这个问题的本质其实是,apt 是怎么运行的?

  1. apt 扫描所有的源码,从中找出所有的注解 → 这些注解称为 未申明的注解
  2. apt 找到所有的 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

来看一个例子


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;
    }

更详细的用法可以参考 javapoet基础用法,或者下面的实践


一个典型自定义 Annotation 的写法

在公司的项目实践中有这样的需求:app 需要与另一个非手机设备连接后才能使用,app 中有许多逻辑是设备连接上后就需要立即执行,设备断链后就停止执行,回收资源。或者有的逻辑比较关心某个特定 Activity 的进入和退出事件。我们称这样的逻辑为 逻辑孤岛(比如读取设备的 SN 号),即它不依赖于其他业务模块,只需要在一个事件起来,在另一个事件结束。

实践中,不要一开始就凭空想象这个新类文件的模样,我们最好先将新生成类文件用实际代码写一份,确保逻辑正确能跑通,然后再写自定义 processor 去生成这份类文件代码。由于 processor 的维护成本可能会高一点,因此新类文件最好能逻辑越少越好,将其余逻辑放在 business module 中。

这是基于上述思路的代码

上一篇下一篇

猜你喜欢

热点阅读