Java 注解处理器

apt

前面,介绍了 Java5.0 引入的注解,现在来介绍注解处理机制。

注解处理机制 = 注解 + 注解处理器

注解处理器和注解一般共同组成 Java Library ,它对外提供了特定的功能。



注解处理器

注解处理器 APT(Annotation Processing Tool),在编译期间,JVM 可以加载注解处理器来处理对应的注解,处理方式一般是根据注解信息生成代码,避免我们重复一些无聊的代码。

先来一张 Android 的 AOP (Aspect-Oriented Programming 面向切面编程) 的图来理解 APT 的作用。

aop

APT 的作用范围只是在源码时期处理注解。

注解处理器在 Java 5 引入,但那时并没有标准化的 API 可用,需通过 APT(Annotation Processing Tool)结合 Mirror API(com.sun.mirror)来实现。

Java 6 开始,注解处理器被规范化为 Pluggable Annotation Processing,定义在 JSR 269 标准中, 在标准库中提供了 API, APT 被集成到 Javac 工具中。


准备

在这之前,需要做一些准备。

一般注解处理器都是对外提供一些功能,而且注解处理器的 API 定义在 Java 的标准库中。

所以会单独新建一个Java Library ,并且 build.gradle 文件添加如下依赖:

apply plugin: 'java'

dependencies {
    compile project(':lib') //依赖的注解
    compile 'com.google.auto.service:auto-service:1.0-rc2'//用于生成 META-INF 配置
    compile 'com.squareup:javapoet:1.4.0'// 用于自动生成代码
}

//  解决build警告:编码GBK的不可映射字符
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

另外,我们的注解也会单独新建一个Java Library 来专门存放,这样注解与注解处理器在两个 Library ,相互隔离。


AbstractorProcessor

注解处理器的 API 定义在 javax.annotation.processing 包中,其中 Processor 接口定义注解处理器,其子类 AbstractorProcessor 是一个抽象类,它额外添加了便捷方法,我们一般使用这个子类进行注解处理。

首先,新建一个自定义注解处理器 MyProcessor,然后继承 AbstractorProcessor ,它有四个重要的方法:

public class MyProcessor extends AbstractProcessor {

    @Override   //init对一些工具进行初始化。
    public synchronized void init(ProcessingEnvironment env){ }

    @Override   //真正处理注解,生成java代码的地方。
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    @Override   //表示该注解处理器可以处理哪些注解
    public Set<String> getSupportedAnnotationTypes() { }

    @Override   //表示可以处理的 java 版本
    public SourceVersion getSupportedSourceVersion() { }

}

上面有注释,每个方法都有其对应的功能。

但现在,我们一般不这么写,现在许多方法可以直接使用注解替代:

@AutoService(Processor.class)   //自动生成 Processor 文件的 META-INF 配置信息。
@SupportedSourceVersion(SourceVersion.RELEASE_8)    //java版本支持
@SupportedAnnotationTypes({ //标注注解处理器支持的注解类型
        "com.deemons.modulerouter.RouterService",
        "com.deemons.modulerouter.RouterLogic",
        "com.deemons.modulerouter.RouterAction"
})
public class AnnotationProcessor extends AbstractProcessor{
    public Filer mFiler; //文件相关的辅助类
    public Elements mElements; //元素相关的辅助类
    public Messager mMessager; //日志相关的辅助类

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mFiler = processingEnv.getFiler();
        mElements = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        return true;
    }
}

可以看到,这里用了许多注解来简化方法,我们先来解释几个注解:

  • @AutoService 是 Google 出的一个库,它主要用于注解 Processor,对其生成 META-INF 配置信息。

    它的引入方式为 compile 'com.google.auto.service:auto-service:1.0-rc2'

  • @SupportedSourceVersion 声明可以处理的Java版本,和getSupportedSourceVersion() 方法的作用相同。

  • @SupportedAnnotationTypes 声明此注解处理器支持的注解,格式必须是 「包名+注解名」。


此外,还有几个重要的类需要解释:

  • processingEnv 其实就是 AbstractProcessor 的成员变量ProcessingEnvironment ,它提供了很多工具类。

  • Filer 文件相关的辅助类,它可以创建文件。

  • Messager 日志相关的辅助类,用于打印注解处理器中的日志到控制台。
  • Elements 元素相关的辅助类。这个类非常重要,下面会重点介绍。


Elements

Elements 元素,源代码中的每一部分都是一个特定的元素类型,分别代表了包、类、方法等等。

Elementd 的每一个子类都代表一种特定类型:

  • PackageElement 表示一个包程序元素。提供对有关包极其成员的信息访问
  • ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
  • TypeElement 表示一个类或接口程序元素。提供对有关类型极其成员的信息访问。
  • TypeParameterElement 表示一般类、接口、方法或构造方法元素的类型参数。
  • VariableElement 表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。

下面来列举一个普通的类:

package com.example;//包 PackageElement    

public class Foo { // 类 TypeElement

    private int a; // 字段 VariableElement
    private Foo other; //字段 VariableElement

    public Foo() {} //方法 ExecuteableElement

    public void setA( //方法 ExecuteableElement
            int newA //参数 TypeParameterElement
    ) {
    }
}

这些 Element 元素,相当于 XML 中的 DOM 树,可以通过一个元素去访问它的父元素或者子元素。

element.getEnclosingElement();// 获取父元素
element.getEnclosedElements();// 获取子元素



Message

Messager 是日志输出工具。

虽然是编译时执行 Processor,但也是可以输入日志信息用于调试的。

Processor 日志输出的位置在编译器下方的 Gradle Console 窗口中。

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {

    //取得Messager对象
    Messager messager = processingEnv.getMessager();

    //输出日志
        System.out.println("=============");
        messager.printMessage(Diagnostic.Kind.OTHER,"Diagnostic.Kind.OTHER");
        messager.printMessage(Diagnostic.Kind.NOTE,"Diagnostic.Kind.NOTE");
        messager.printMessage(Diagnostic.Kind.MANDATORY_WARNING,
                                        "Diagnostic.Kind.MANDATORY_WARNING");
        messager.printMessage(Diagnostic.Kind.WARNING,"Diagnostic.Kind.WARNING");
        messager.printMessage(Diagnostic.Kind.ERROR,"Diagnostic.Kind.ERROR");
    }

Messager也有日志级别的选择。

  • Diagnostic.Kind.OTHER
  • Diagnostic.Kind.NOTE
  • Diagnostic.Kind.MANDATORY_WARNING
  • Diagnostic.Kind.WARNING
  • Diagnostic.Kind.ERROR注解处理器出错,打印此日志后,编译会失败。

打印的结果如下:

注: Diagnostic.Kind.OTHER
注: Diagnostic.Kind.NOTE
警告: Diagnostic.Kind.MANDATORY_WARNING
警告: Diagnostic.Kind.WARNING


process()

process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 是注解处理器的核心方法,必须实现。

APT 会扫描源码中所有的相关注解,然后会回调process() 这个方法,如果没有扫描到声明的相关注解,则不会回调此方法。

它的实现一般可以分为两个步骤,首先收集信息,然后根据收集的信息生成代码。

  • 收集信息主要依靠 RoundEnvironment 来获取想要的 Elements 。
  • 生成代码则需要用到一个库 Javapoet


先介绍这个方法发两个参数Set<? extends TypeElement>RoundEnvironment


Set<? extends TypeElement>

将返回所有的由该Processor处理,并待处理的 Annotations。

属于该Processor处理的注解,但并未被使用,不存在与这个集合里。


RoundEnvironment

表示当前或是之前的运行环境,可以通过该对象查找到需要的注解。

常使用的方式是:

for (Element element : roundEnv.getElementsAnnotatedWith(RouterService.class)) {
     //取出所有被@RouterService 标记的元素,然后遍历 
}

Element 中并没有子类特定的扩展方法,这时候往往需要强制类型转换。

如果能确定取出的元素类型,一般可以这样:

Set<? extends Element> annotatedWith = roundEnv.getElementsAnnotatedWith(RouterService.class);
for (TypeElement typeElement : ElementFilter.typesIn(annotatedWith)) {
     help.serviceElement = typeElement;
     help.processName = typeElement.getAnnotation(RouterService.class).value();//获取注解的值
}

ElementFilter 是一个工具类,它能过滤出特定类型的元素。

这种方式比强制类型转换优雅许多,推荐使用这种工具类。



Javapoet

Javapoet 是 square 公司开源的库,他能优雅的生成代码,让你从重复无聊的代码中解放出来。

如果英文好,可以直接看 Github 上的文档。依赖方式:

compile 'com.squareup:javapoet:1.4.0'


基础

javapoet 里面常用的几个类:

  • MethodSpec 代表一个构造函数或方法声明。
  • TypeSpec 代表一个类,接口,或者枚举声明。
  • FieldSpec 代表一个成员变量,一个字段声明。
  • JavaFile包含一个顶级类的 Java 文件。

如果我们想生成下面的类:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

那么,我们使用 Javapoet 如下:

MethodSpec main = MethodSpec.methodBuilder("main")//构造名为 main 方法
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//添加关键字public、final、static等,可添加多个
    .returns(void.class)//设置方法发返回的类型
    .addParameter(String[].class, "args")//设置方法的参数
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//在方法里添加语句。
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")//构造名为 HelloWorld 的类 
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)//添加关键字
    .addMethod(main)//添加方法
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)//在指定位置构建文件
    .build();

javaFile.writeTo(processingEnv.getFiler());//写入文件里

上面就是一些基础方法是使用,比如设置关键字addModifiers、设置返回类型returns、添加方法参数addParameter、添加语句addStatement等,以上都有注释。


复杂参数

有时候,我们的方法有复杂的参数,那如何定义呢?比如集合

  MethodSpec method = MethodSpec.methodBuilder("testList")
                .addModifiers(Modifier.PUBLIC)
                .returns(TypeName.VOID)
                .addParameter(ParameterizedTypeName.get(
                        ClassName.get(ArrayList.class),
                        ClassName.get(packageName, "TestBean"))
                        , "beanList")
                .build();

效果如下:

public void testList(ArrayList<TestBean> beanList){
}

以上只是简单的 ArrayList ,如果是更加复杂的 HashMap 呢?而且如果里面继续嵌套ArrayList 呢?

  MethodSpec method = MethodSpec.methodBuilder("testHashMap")
                .addModifiers(Modifier.PUBLIC)
                .returns(TypeName.VOID)
                .addParameter(ParameterizedTypeName.get(
                        ClassName.get(HashMap.class),
                        ClassName.get(String.class),
                        ParameterizedTypeName.get(
                            ClassName.get(ArrayList.class),
                            ClassName.get(packageName,"TestBean"))
                        ), "hashMap")
                .build();

效果如下:

public void testHashMap(HashMap<String, ArrayList<TestBean> hashMap){
}

可以看到,ParameterizedTypeName 可以无限循环嵌套,任何复杂的参数,都可以使用ParameterizedTypeName 自定义出来。


复杂变量

我们首先定义一个简单的成员变量

TypeSpec helloWorld = TypeSpec.classBuilder("test")
    .addModifiers(Modifier.PUBLIC)
    .addField(String.class, "name", Modifier.PRIVATE, Modifier.FINAL) //添加成员变量
    .build();

结果如下:

public class test(){
    private final String name;
}

这是简单的添加变量,如果还需要对变量进行赋值呢?

FieldSpec fielSpec = FieldSpec.builder(String.class, "version")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    .initializer("$S + $L", "Lollipop v.", 5.0d) //$S $L 都是占位符
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("test")
    .addModifiers(Modifier.PUBLIC)
    .addField(fielSpec) //添加成员变量
    .build();

结果如下:

public class test(){
    private final String name = "Lollipop v." + 5.0;
}


控制流

如果遇到判定、循环、选择等,可以使用beginControlFlowendControlFlow,就像这样

MethodSpec main = MethodSpec.methodBuilder("main")
    .addStatement("int total = 0")  //添加语句
    .beginControlFlow("for (int i = 0; i < 10; i++)")   //开始控制流,即此语句后添加大括号 {
    .addStatement("total += i") //在控制流里面添加语句
    .endControlFlow()   //结束流,即 }
    .build();

效果如下:

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}


构造方法

构造方法使用 MethodSpec.constructorBuilder()

MethodSpec flux = MethodSpec.constructorBuilder()   //创建构造方法
    .addModifiers(Modifier.PUBLIC)  //添加关键字
    .addParameter(String.class, "greeting") //添加方法参数
    .addStatement("this.$N = $N", "greeting", "greeting")    //添加语句
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(String.class, "greeting", Modifier.PRIVATE, Modifier.FINAL) //添加成员变量
    .addMethod(flux)
    .build();

结果如下:

public class HelloWorld {
  private final String greeting;

  public HelloWorld(String greeting) {
    this.greeting = greeting;
  }
}


占位符

我们经常会用到占位符,这里有四种占位符:

  • $S for Strings 用于字符串等。
  • $T for Types 用于类。例如使用字节码Class来填写类 new $T()
  • $N for Names 用于自己生成的方法名或者变量名等等
  • $L for Literals 用于字面值,例如使用类元素TypeElement来填写 $L.class

这些占位符需要多运用才能理解,特别是$L 字面量,有点难理解,却又用途广泛。

其他

还有一些其他比较常用的方法:

  • MethodSpec.addAnnotation(Override.class); 方法上面添加注解
  • TypeSpec.enumBuilder("XXX") 生成一个XXX的枚举
  • TypeSpec.interfaceBuilder("HelloWorld") 生成一个 HelloWorld 接口
  • TypeSpec.addSuperinterface() 继承一个类,或者实现抽象方法、接口。


至此,使用 Javapoet 来生成代码也就无压力了。


处理过程

这里说一下注解处理器的一个流程。

APT可以扫描源码中的所有注解,并将相关的注解回调到注解处理器中的process() 方法中,我们依据这些注解来提取信息,并生成代码,然后添加到源码中。

但如果生成的代码中也有注解,那么仅扫描一次源码肯定是有问题的。为了能避免这一问题,APT 至少会扫描两次源码。如果第二次扫描后继续生成有注解的代码,那么类似递归一样会再次扫描,直到不再出现新注解。所以,这个流程可以无限递归。

因此,在生成的代码中如果需要添加注解,一定要慎重,避免出现死循环。



annotationProcessor

注解处理器写出来后,需要IDE 来加载这些注解处理器。

刚开始,Android studio 中加载注解处理器的方式是使用第三方插件 android-apt ,但随着 Android Gradle 插件 2.2 版本发布之后, android-apt 作者在官网发表声明称,后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt


现在看看 annotationProcessor 的使用方式:

首先,确保 Android 工程的 Gradle 插件版在 2.2 及以上,

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.1'
    }
}

其次,使用annotationProcessor 声明注解依赖,以依赖 Dagger 2 为例:

dependencies {
    compile 'com.google.dagger:dagger:2.0'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.0'
}

可以看到,现在使用annotationProcessor 相较于以前使用android-apt 方便了很多。



参考

猜你喜欢

转载自blog.csdn.net/deemons/article/details/78188013