Kahn's blogs

Annotation Processing Tool

2017/12/05

上一篇说了ASM,直接修改字节码,说实话,直接修改字节码真的不好(各种版本问题,Android的兼容也不是很好,最近android新出的jack工具链编译系统也不知道支持不支持)。在文章的后面也说了,能用代码(设计模式)解决的问题“尽量”用代码解决。那么,我们使用设计模式的时候,难免会出现大量的套路代码,怎么破?APT来了。这玩意是在编译前会扫描所有注解,给你一个生成java源代码,源代码就不会出现这些问题了,跟你手写的一样!来吧,让我们看看apt是啥。

本篇主要不是讲基础的APT,主要说google auto工具库,javapoet。

惯例 轮子
Java注解(Annotation)
Java注解处理器

APT

Annotation Processing Tool其实就是一个javac提供的注解处理器,他会扫描所有源代码中的注解。那么是怎么触发的呢?

首先,你想让javac扫描你的注解,就必须提供一个注解处理器,在代码中的体现就是继承javax.annotation.processing.AbstractProcessor类,实现里面的process抽象方法。javac就会在编译前,把所有的注解通过参数都给你,当然你也可以用getSupportedAnnotationTypes过滤出你想要的。然后再提供一个META-INF文件,给javac说明一下你的哪个类是注解处理器就可以了。因为这个处理是在编译前,你不用文件说明一下javac也不知道呀。

看看,APT是不是很简单?就是给javac提供一个AbstractProcessor实现,它会给你想要的。AbstractProcessor实现了Processor接口,看看Processor接口。

init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。后面我们将看到详细的内容。
process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。我推荐你使用前者。
上面的解释来自开篇推荐的博客

在init方法中,参数ProcessingEnviroment可以获取一些辅助对象,可以把这些对象缓存下来,以便在process中使用。

主要关注的就是process方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<String, FactoryGroupedClasses>();
...
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 遍历所有被注解了@Factory的元素
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
...
}
}
...
}

借用一下开篇博客的例子,一般的处理方式就是在process里面过滤出你想处理的注解。得到的是一个Element。关于Element的概念,大家可以仔细研读开篇的推荐博客。(不得不说,开篇的博客写的真的很好,把AbstractProcessor的细节都讲的很清楚)。

后面就是使用Filer来生成java源文件。APT的基础应用大概就是这样子,你可以生成套路代码(Butter Knife),或者按照约定生成一些相互对应的类,来实现模块之间的解耦。或者是像dagger那样,生成一套注入框架。

Auto

Google Auto是一套Apt的工具类,可以使你的apt开发过程更清爽,爽。

AutoService

上面的开篇博客讲了auto其中一个套件的功能,就是@AutoService(Processor.class)注解。在一个AbstractProcessor实现类上写上这样的注解,你就可以不用配置META-INF文件夹的内容了,auto会帮你自动实现

Common

common里面给我们提供了可以模块化开发apt的一些扩展实现。其中有个BasicAnnotationProcessor类。原本你要继承AbstractProcessor,现在只要继承BasicAnnotationProcessor就可以了。它有一个抽象方法,你需要实现一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class TestAptProcessor extends BasicAnnotationProcessor {
@Override
protected Iterable<? extends ProcessingStep> initSteps() {
return ImmutableList.of(new TestProcessingStep(), new Test2ProcessingStep());
}
}

返回值是一个类型为ProcessingStep的集合。那么ProcessingStep又是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* The unit of processing logic that runs under the guarantee that all elements are complete and
* well-formed. A step may reject elements that are not ready for processing but may be at a later
* round.
*/
public interface ProcessingStep {
/** The set of annotation types processed by this step. */
Set<? extends Class<? extends Annotation>> annotations();
/**
* The implementation of processing logic for the step. It is guaranteed that the keys in
* {@code elementsByAnnotation} will be a subset of the set returned by {@link #annotations()}.
*
* @return the elements that this step is unable to process, possibly until a later processing
* round. These elements will be passed back to this step at the next round of processing.
*/
Set<Element> process(SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation);
}

ok,它里面有两个方法,一个annotations,返回了一个注解set。你可以返回一些注解。一个是process,它的参数是一个map,key是一个注解,value是一个Element。看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Created by kahn on 2017/12/6.
*/
public class TestProcessingStep implements BasicAnnotationProcessor.ProcessingStep {
@Override
public Set<? extends Class<? extends Annotation>> annotations() {
return ImmutableSet.of(Test.class);
}
@Override
public Set<Element> process(SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation) {
System.out.println("------TestProcessingStep process-------");
System.out.println(elementsByAnnotation);
ImmutableSet.Builder<Element> rejectedElements = ImmutableSet.builder();
return rejectedElements.build();
}
}

编译后,它的输出为

1
2
------TestProcessingStep process-------
{interface com.kahn.aptannotation.Test=[com.kahn.testapt.MainActivity]}

我还有一个Test2ProcessingStep,它的输出为

1
2
------Test2ProcessingStep process-------
{interface com.kahn.aptannotation.Test2=[com.kahn.testapt.MainActivity2]}

这下应该一目了然了吧。

ProcessingStep可以把你需要处理的注解独立出来单独处理,每个ProcessingStep只需要关注自己想关注的注解。这不是很好的解耦吗?如果按照以前的写法,处理所有注解都挤在一个process里面。

ProcessingStep的process方法的返回值也需要重点说一下。它的返回值是需要进入下一轮循环的Element集合。例如:我们在本轮生成了一些带有注解的源代码,我们得保证这些源代码跟我们之前的源代码一样也经过apt的扫描。也可以返回一些目前无法处理的元素,例如:你要处理一个类,但是处理这个类需要先处理别的类。那么你就要把这个类放入下一轮进行处理

因为我们的元注解Target只能指定到Type级别的,那么接口interface是一个type,类也是一个class。那么这里已经分开的Element还是有各种各样的Type,你依然需要使用element.getKind().equals(INTERFACE)这样的代码来过滤出你想要的东西。你可能还想过滤出这个类的访问权限,像这样getModifiers().contains(ABSTRACT)。

当过滤出你真正想要的东西后,就可以从这个TypeElement上获取到注解信息,element.getAnnotation(注解)。然后就是从注解中取值,构建你的源代码。这里有个需要注意的地方,如果你的注解上有个字段是个Class类型的。像这样:

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface Test {
Class<?> getType();
}

那么就要小心了,从这个注解上取到的Class对象,有可能还没被编译!这种情况下,直接获取Class会抛出MirroredTypeException异常。幸运的是,MirroredTypeException包含一个TypeMirror,它表示我们未编译类。因为我们已经知道它必定是一个类类型(我们已经在前面检查过),我们可以直接强制转换为DeclaredType,然后读取TypeElement来获取合法的名字。下面该轮到写源代码了。

Javapoet

Github square/javapoet
javapoet——让你从重复无聊的代码中解放出来

第一个链接是javapoet的github地址。第二个是一篇博文。这篇博文基本把GitHub上的readme.mk翻译了一遍。

用Javapoet就可以轻松写源码了

先说说apt自带的写源码工具。上面有说过一个filter,可以在init中拿到。下面是示例

1
2
3
4
5
JavaFileObject fileObject = filer.createSourceFile("类名", (Element[]) null);
Writer writer = fileObject.openWriter();
writer.write(“类的代码”, value));
writer.flush();
writer.close();

就是这样。

Javapoet就不详细说了,官方的文档还是很全的,总之就是用它的api拼出一个java源代码。当然了,你也可以自己拼代码,就像上面用java原生jdk生成的那样。自己拼代码对一些语法检测无法掌控。

上一个官方的例子:

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
17
18
19
20
21
22
23
private void writeJava() {
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}

javaFile.writeTo(filer),为最终输出。javaFile.writeTo有很多重载方法。这里直接把apt给我们的filer传进去,直接就在目标目录生成一个java文件,就可以用了!

最后

java的apt大概就是这么多内容,当然还有很多细节,比如Element这边没有详细说,因为推荐博客里面已经说的很详细了。原理很简单,但是想设计出优秀的框架还是不容易。如果在写代码的时候,多用设计模式,解耦抽象,你会发现很多代码都是套路代码,可以自动生成的。那么就用apt吧。

首先要清楚的了解,这套机制是基于注解的,所以注解的知识点要牢牢掌握。其次,注解的基本知识只是让你会用。如果想用好,还要去了解Java的注解规范,使用一些JSR规范中提供的注解。例如:依赖注入的JSR 330。

我哔哔完了

本篇博文例子源代码