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 文件添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
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 ,它有四个重要的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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() { }

}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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常量、方法或构造方法参数、局部变量或异常参数。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 树,可以通过一个元素去访问它的父元素或者子元素。

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



Message

Messager 是日志输出工具。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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注解处理器出错,打印此日志后,编译会失败。

打印的结果如下:

1
2
3
4
注: 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

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

常使用的方式是:

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

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

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

1
2
3
4
5
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 文件。

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

1
2
3
4
5
6
7
package com.example.helloworld;

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

那么,我们使用 Javapoet 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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等,以上都有注释。


复杂参数

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

1
2
3
4
5
6
7
8
MethodSpec method = MethodSpec.methodBuilder("testList")
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(ParameterizedTypeName.get(
ClassName.get(ArrayList.class),
ClassName.get(packageName, "TestBean"))
, "beanList")
.build();

效果如下:

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

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

1
2
3
4
5
6
7
8
9
10
11
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();

效果如下:

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

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


复杂变量

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

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

结果如下:

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

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

1
2
3
4
5
6
7
8
9
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();

结果如下:

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


控制流

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

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

效果如下:

1
2
3
4
5
6
void main() {
int total = 0;
for (int i = 0; i < 10; i++) {
total += i;
}
}


构造方法

构造方法使用 MethodSpec.constructorBuilder()

1
2
3
4
5
6
7
8
9
10
11
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();

结果如下:

1
2
3
4
5
6
7
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 及以上,

1
2
3
4
5
6
7
8
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.1'
}
}

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

1
2
3
4
dependencies {
compile 'com.google.dagger:dagger:2.0'
annotationProcessor 'com.google.dagger:dagger-compiler:2.0'
}

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



参考