说起注解处理器,Android程序员都比较兴奋,因为在开发过程中我们常用的一些明星框架,例如ButterKnife
、EventBus
、Dagger
以及阿里的ARouter
都采用是注解处理器技术。简单注解,简单的api,超高的性能等诸多优点,本文就带你从整体出发探讨以下APT技术是怎么玩的。
什么是APT
APT全称“Annotation Processing Tool”,即注解处理器,是javac的一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,并根据注解自动生成代码,帮助开发者减少了很多重复代码的编写。
现在很多流行的框架都是用的这个思想,比如Butterknife、Dragger、Room、组件化框架都是用的编译时注解原理,自动生成了代码,简化了使用。
通俗理解:根据规则,帮助我们生成代码、生成类文件
什么是Element
Element指得是节点或者元素,我们常常把html语言成为结构体语言,因为html语言中有很多规范的标签限定,每一种标签都是一个element:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>i猩人</title>
</head>
<body>
<div>...</div>
</body>
</html>
对于java源文件来说,同样也是一种结构体语言,源代码的每一个部分都是一个特定类型的Element,也就是说Element代表源文件中的元素,例如包、类、字段、方法等。java的Element是一个接口,由Element衍生出来的扩展类共有5种:
package com.example; // PackageElement 包元素/节点
public class Main<T> { // TypeElement 类元素/节点; 其中<T>属于TypeParameterElement 泛型元素/节点
private int x; // VariableElement 变量、枚举、方法参数元素/节点
public Main() { // ExecuteableElement 构造函数、方法元素/节点
}
}
- PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
- TypeElement 表示一个类或者接口程序元素。提供对有关类型及其成员信息的访问。
- TypeParameterElement 表示一个泛型元素
- VariableElement 表示一个字段、enum常量、方法或者构造方法的参数、局部变量或异常参数
- ExecuteableElement 表示某个类或者接口的方法、构造方法或初始化程序(静态或者实例)
Element是一个接口,常用的api如下:
public interface Element extends AnnotatedConstruct {
// 获取元素的类型,实际的对象类型
TypeMirror asType();
// 获取Element的类型,判断是哪种Element
ElementKind getKind();
// 获取修饰符,如public static final等关键字
Set<Modifier> getModifiers();
// 获取名字,不带包名
Name getSimpleName();
// 返回包含该节点的父节点,与getEnclosedElements()方法相反
Element getEnclosingElement();
// 返回该节点下直接包含的子节点,例如包节点下包含的类节点
List<? extends Element> getEnclosedElements();
boolean equals(Object var1);
int hashCode();
List<? extends AnnotationMirror> getAnnotationMirrors();
<A extends Annotation> A getAnnotation(Class<A> var1);
<R, P> R accept(ElementVisitor<R, P> var1, P var2);`
}
既然源文件是一种结构化的数据,那么针对某个Element我们可以获取它的父元素或者子元素:
TypeElement person= ... ;
// 遍历它的孩子
for (Element e : person.getEnclosedElements()){
// 拿到孩子元素的最近的父元素
Element parent = e.getEnclosingElement();
}
我们发现Element有时会代表多种元素,例如TypeElement代表类或接口,此时我们可以通过element.getKind()来区分:
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
for (Element element : elements) {
if (element.getKind() == ElementKind.CLASS) {
// 如果元素是类
} else if (element.getKind() == ElementKind.INTERFACE) {
// 如果当前元素是接口
}
}
ElementKind枚举声明有这些:
枚举类型 | 种类 |
---|---|
PACKAGE | 包 |
ENUM | 枚举 |
CLASS | 类 |
ANNOTATION_TYPE | 注解 |
INTERFACE | 接口 |
ENUM_CONSTANT | 枚举常量 |
FIELD | 字段 |
PARAMETER | 参数 |
LOCAL_VARIABLE | 本地变量 |
EXCEPTION_PARAMETER | 异常参数 |
METHOD | 方法 |
CONSTRUCTOR | 构造函数 |
OTHER | 其他 |
其他Element如VariableElement特殊的api:
- getConstantValue() :获取初始化变量的值。
其他的都相对简单一些。
注解处理器实现过程
一般的apt框架都是习惯建立三个module,两个java module,一个Android lib module:
- apt-annotation 存放注解
- apt-processor 存放自定义注解处理器,java代码编译生成规则在这里声明
- apt-api 暴露给用户的api,我们生成的java代码怎么调用,需要提供api支持
其中apt-processor需要依赖apt-annotation,因为要用到apt-annotation中的相关注解,通过注解获取更多的类信息
接下来我们理一下注解处理器的实现过程,总的来说需要以下几步:
- 注解处理器声明
- 注解处理器注册
- 注解处理器文件生成
- 注解处理器调用
注解处理器声明
每一个注解处理器都要继承于AbstractProcessor
,然后实现以下五个方法:
public class MyProcessor extends AbstractProcessor {
/**
* 注解处理器初始化方法,相当于Activity的onCreate方法。
*
* @param processingEnvironment 该入参可以提供若干工具类,供将来编写代码生成规则时所使用
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
/**
* 声明注解处理器生成java代码规则,在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
*
* @param set 支持处理的注解集合。
* @param roundEnvironment 表示当前或是之前的运行环境,可以通过该对象查找指定注解下的节点信息。
* @return 如果返回 true,则这些注解已处理,后续 Processor 无需再处理它们;如果返回 false,则这些注解未处理并且可能要求后续 Processor 处理它们。
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
/**
* 返回一个当前注解处理器所有支持的注解的集合。当前注解处理器需要处理哪种注解就加入那种注解。如果类型符合,就会调用process()方法。
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
/**
* 需要通过那个版本的jdk来进行编译
*
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
/**
* 接收外来传入的参数,最常用的形式就是在Gradle里`javaCompileOptions`
*
* @return
*/
@Override
public Set<String> getSupportedOptions() {
return super.getSupportedOptions();
}
}
其中getSupportedAnnotationTypes()
、getSupportedSourceVersion()
和getSupportedOptions()
还可以采用注解进行声明,例如:
@SupportedAnnotationTypes({"com.simple.annotation.MyAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedOptions("MODULE_NAME")
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
其中MyAnnotation
就是我们注解处理器所支持的注解,是个列表,开发中我们所使用的注解只要在这个列表内就行就能被注解处理器接收到,进而通过这些注解获取更多的节点信息;MODULE_NAME
是我们在gradle中注册的外部变量,便于编译的时候注解处理器能够引用到,例如大名鼎鼎的ARouter在build.gradle的defaultConfig闭包中注册的AROUTER_MODULE_NAME
变量,用于将来生成路由表分组所需:
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
注解处理器注册
怎样将注解处理器注册到Java编译器中呢?这里有两种方式:手动注册和自动注册。
手动注册属于古老基本的注册方式,就是把所有的自定义注解处理器打包成一个jar包,然后通过引用此jar包来生成相应的java代码。在此之前你需要声明一个特定的文件javax.annotation.processing.Processor到META-INF/services路径下,一同打包到jar包中。
META-INF/services 相当于一个信息包,目录中的文件和目录获得Java平台的认可与解释用来配置应用程序、扩展程序、类加载器和服务文件,在jar打包时自动生成
其中javax.annotation.processing.Processor文件中的内容为每个注解处理器的合法的全名列表,每一个元素换行分割
com.simple.processor.MyProcessor
com.simple.processor.MyProcessor1
com.simple.processor.MyProcessor2
自动注册比较简单,只需要在apt-processor模块的build-gradle中声明一条依赖:
implementation 'com.google.auto.service:auto-service:1.0-rc4'
然后在相应的注解处理器的类名上添加一条注解@AutoService(Processor.class)
,即可:
@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.simple.annotation.MyAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedOptions("MODULE_NAME")
public class MyProcessor extends AbstractProcessor {
...
注解处理器文件生成
这一步是APT技术的精华所在,编写我们想要的java代码对应的生成规则。开篇我们已经提到了Element节点的概念,编译的过程同样是根据源文件节点信息来过滤和解析,当然生成java代码也同样需要过滤节点信息,然后根据规则拼接。
还记得我们声明自定义注解器的init
方法吗?在这里我们一般会实例化几个工具:
/**
* 节点工具类(类、函数、属性都是节点)
*/
private Elements mElementUtils;
/**
* 类信息工具类
*/
private Types mTypeUtils;
/**
* 文件生成器
*/
private Filer mFiler;
/**
* 日志信息打印器
*/
private Messager mMessager;
private String mModuleName;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnv.getElementUtils();
mTypeUtils = processingEnv.getTypeUtils();
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
mModuleName = processingEnv.getOptions().get("MODULE_NAME");
}
其中:
- mElementUtils 节点工具类,获取指定的节点信息,通过节点信息可以进一步获取该节点的类型、名称等。
- mTypeUtils 类信息工具类,常用于类型判断
- mFiler 文件生成器,生成指定的文件
- mMessager 日志工具类,用于打印日志信息
接下来便可以用这几个工具在process
方法里大展身手了。注解处理器文件生成规则主要在process
方法中实现,文件生成规则实现方式也有两种:
- 常规的写文件方式。
- 利用javapoet框架来编写。
例如想生成以下这个文件:
package com.example.helloworld;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, APT!");
}
}
常规的写文件方式,采用Writer.write实现,相应比较死板,需要把每一个字母都写上,有时候甚至连导包的import
代码也要一字不差的垒上,这种方式写起来痛苦,容易出错,当然优点就是比较清晰一目了然:
StringBuilder builder = new StringBuilder()
.append("package com.example.helloworld;\n\n")
.append("public final class HelloWorld{\n")
.append("\tpublic static void main(String[] args) {\n")
.append("\t\tSystem.out.println(\"Hello, APT!\");\n")
.append("\t}\n")
.append("}");
Writer writer = null;
try {
JavaFileObject source = mFiler.createSourceFile("com.example.helloworld");
writer = source.openWriter();
writer.write(builder.toString());
} catch (IOException e) {
throw new RuntimeException("APT process error", e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
//Silent
}
}
}
大名鼎鼎的EventBus就是这种处理的,甚至最新的3.0版本方式也没有改变。
利用square团队的javapoet
框架写文件方式更像编辑器写代码一样,更符合程序员的感觉,推荐!JavaPoet常用类如下:
- TypeSpec————用于生成类、接口、枚举对象的类
- MethodSpec————用于生成方法对象的类
- ParameterSpec————用于生成参数对象的类
- AnnotationSpec————用于生成注解对象的类
- FieldSpec————用于配置生成成员变量的类
- ClassName————通过包名和类名生成的对象,在JavaPoet中相当于为其指定Class
- ParameterizedTypeName————通过MainClass和IncludeClass生成包含泛型的Class
- JavaFile————控制生成的Java文件的输出的类
JavaPoet的生成规则如下:
// 构建主函数
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, APT!") // 构建方法体
.build();
// 构建类
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL) // 指定类修饰符
.addMethod(main) // 添加方法
.build();
// 指定包路径,构建文件体
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
// 创建文件
javaFile.writeTo(System.out);
JavaPoet详细的使用文档内容比较多,这里不做过多的描述,可以查看官方文档:github.com/square/java… ,当然也有网友专门整理了一些中文教程:blog.csdn.net/l540675759/…
无论用哪种方式编写我们的java代码生成规则,我建议都先写一个指定java代码模板,然后根据此模板进行编写。
另外,写java代码生成规则之前我们也要做一件事,就是对注解处理器所支持的注解进行获取并解析,获取必要的节点信息,然后将这些信息组合到我们的生成规则中,这样我们的注解才会有意义:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set == null || set.isEmpty()) {
return false;
}
Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(MyAnnotation.class);
StringBuilder annotations = new StringBuilder();
if (rootElements != null && !rootElements.isEmpty()) {
for (Element element : rootElements) {
annotations.append(element.getSimpleName() + ",");
}
}
String s = annotations.toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "所有注解的类信息:" + s);
// 构建主函数
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC) // 指定方法修饰符
.returns(void.class) // 指定返回类型
.addParameter(String[].class, "args") // 添加参数
.addStatement("$T.out.println($S)", System.class, s) // 构建方法体
.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(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
注解处理器调用
关于调用,上边也说过,我们专门新建一个名为apt-api的android module,为什么要写api呢?首先我们写代码的时候这些文件还没有编译生成,我们无法直接用。第二,APT是一个动态框架,也就是说开发不需要关心到底生成什么鬼东西,只要告诉我怎么用就行,生成千千万个文件都是内部的细节,开发不需要操心。
例如我们想实例化上边的HelloWorld,我们可以这样编写相应的api:
public class MyAptApi {
public static void init() {
try {
Class<?> c = Class.forName("com.example.helloworld.HelloWorld");
c.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
开发只需要关注MyAptApi的init()方法,在合适的地方调用即可:
MyAptApi.init();
一般情况下我们想要使用我们生成的java类文件,更多的是将类文件实现某个现成的接口,通过接口代理来使用。
最后
注解处理器APT技术使用很广泛,有些框架将其运用的看起来“百思不得其解”,但是总归有个套路,本篇文章仅仅从apt的整体结构上阐释其基本使用过程,师傅领进门修行靠个人,更深层次的还望各位童鞋进一步挖掘,最好是透过一些框架原理去深层次学习,例如ButterKnife和EventBus。
安利两篇别人总结很好的文章: