上一篇说了ASM,直接修改字节码,说实话,直接修改字节码真的不好(各种版本问题,Android的兼容也不是很好,最近android新出的jack工具链编译系统也不知道支持不支持)。在文章的后面也说了,能用代码(设计模式)解决的问题“尽量”用代码解决。那么,我们使用设计模式的时候,难免会出现大量的套路代码,怎么破?APT来了。这玩意是在编译前会扫描所有注解,给你一个生成java源代码,源代码就不会出现这些问题了,跟你手写的一样!来吧,让我们看看apt是啥。
本篇主要不是讲基础的APT,主要说google auto工具库,javapoet。
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 | (Processor.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 | (Processor.class) |
返回值是一个类型为ProcessingStep的集合。那么ProcessingStep又是什么呢?
1 | /** |
ok,它里面有两个方法,一个annotations,返回了一个注解set。你可以返回一些注解。一个是process,它的参数是一个map,key是一个注解,value是一个Element。看一个例子:
1 | /** |
编译后,它的输出为
1 | ------TestProcessingStep process------- |
我还有一个Test2ProcessingStep,它的输出为
1 | ------Test2ProcessingStep process------- |
这下应该一目了然了吧。
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 |
|
那么就要小心了,从这个注解上取到的Class对象,有可能还没被编译!这种情况下,直接获取Class会抛出MirroredTypeException异常。幸运的是,MirroredTypeException包含一个TypeMirror,它表示我们未编译类。因为我们已经知道它必定是一个类类型(我们已经在前面检查过),我们可以直接强制转换为DeclaredType,然后读取TypeElement来获取合法的名字。下面该轮到写源代码了。
Javapoet
Github square/javapoet
javapoet——让你从重复无聊的代码中解放出来
第一个链接是javapoet的github地址。第二个是一篇博文。这篇博文基本把GitHub上的readme.mk翻译了一遍。
用Javapoet就可以轻松写源码了
先说说apt自带的写源码工具。上面有说过一个filter,可以在init中拿到。下面是示例
1 | JavaFileObject fileObject = filer.createSourceFile("类名", (Element[]) null); |
就是这样。
Javapoet就不详细说了,官方的文档还是很全的,总之就是用它的api拼出一个java源代码。当然了,你也可以自己拼代码,就像上面用java原生jdk生成的那样。自己拼代码对一些语法检测无法掌控。
上一个官方的例子:
1 | package com.example.helloworld; |
生成这个类的javapoet代码
1 |
|
javaFile.writeTo(filer),为最终输出。javaFile.writeTo有很多重载方法。这里直接把apt给我们的filer传进去,直接就在目标目录生成一个java文件,就可以用了!
最后
java的apt大概就是这么多内容,当然还有很多细节,比如Element这边没有详细说,因为推荐博客里面已经说的很详细了。原理很简单,但是想设计出优秀的框架还是不容易。如果在写代码的时候,多用设计模式,解耦抽象,你会发现很多代码都是套路代码,可以自动生成的。那么就用apt吧。
首先要清楚的了解,这套机制是基于注解的,所以注解的知识点要牢牢掌握。其次,注解的基本知识只是让你会用。如果想用好,还要去了解Java的注解规范,使用一些JSR规范中提供的注解。例如:依赖注入的JSR 330。
我哔哔完了