Annotation Processor
大量框架(例如Room, Dagger2, DataBinding, Butterknife)都利用Java1.6引入的annotation processor(后文统称注解处理器)来实现自己的框架功能。
这篇文章简单介绍注解处理器
注解处理器的运行原理
整个代码生成的过程发生在编译期,javac会获知所有的注解处理器,同时在编译时处理所有的注解。
由于整个过程在编译期完成,你可以认为注解处理器是编译器的一部分,因此最后的编译产物是不会包含编译处理器的内容的。
实现一个简单的注解处理器
基本步骤
- 创建一个注解module(Java Library),这个module只包含注解(Annotation)
- 创建一个处理器module(Java Library),这个module包含相关注解的处理逻辑
解决依赖
注解相关的module不需要任何依赖。
处理器module需要依赖我们自定义的注解和一些工具库。上文中提到过,这个module将会在编译器完成自己的任务,因此依赖此module的app不会打包这个module的内容。所以不用担心这个module的包体积问题
1 | implementation fileTree(dir: 'libs', include: ['*.jar']) |
之后给我们的app添加上注解处理器的编译依赖,这里使用另外一个依赖关键字annotationProcessor
1 | implementation project(':annotation') |
annotationProcessor
表示我们只需要编译期有这个依赖,不需要打包进apk
创建注解
这里复习一下Java中注解的相关概念
@Target
:指明你的注解作用的对象,可以有许多类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public enum ElementType {
TYPE, //If you want to annotate class, interface, enum..
FIELD, //If you want to annotate field (includes enum constants)
METHOD, //If you want to annotate method
PARAMETER, //If you want to annotate parameter
CONSTRUCTOR, //If you want to annotate constructor
LOCAL_VARIABLE, //..
ANNOTATION_TYPE, //..
PACKAGE, //..
TYPE_PARAMETER, //..(java 8)
TYPE_USE; //..(java 8)
private ElementType() {
}
}
@Retention
:指明注解如何存储,有三种存储方式
- SOURCE—用于编译期,不会保存
- CLASS—保存到最后的class文件,运行期不会保留
- RUNTIME—保存到class文件,同时运行期也会保留(用于反射)
我们这个场景只需要编译期注解:
1 | (RetentionPolicy.SOURCE) |
创建注解处理器
在注解处理器模块添加一个新类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package com.github.tedaliez.processor;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
public class NewIntentProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
init()
初始化的回调。在这个方法中我们创建了四个类型的引用:
- Elements:Element(下文介绍)的工具类
- Types:TypeMirror(下文介绍)的工具类
- Filer:用来创建文件
- Messager:用来当注解处理器出现编译错误时打印错误信息
Element
注解处理过程中我们会扫描我们的java源代码。每个代码都有一个确定的Element类型,换句话说,Elment代表了一个程序的语言层级的元素
1 | package com.example; // PackageElement |
但是你不能从Element中获取类的相关信息,例如类的父类关系。这类信息得从TypeMirror中获得
Messager
用来当注解处理器出现编译错误时打印错误信息,需要注意的是当有异常出现时,你的注解处理器也应该正常结束而非选择抛出异常。如果抛出异常结束,那么javac会打印你的堆栈,但你messager的信息则不会被打印出来
process()
注解处理器中最重要的方法,方法提供了所有被注解的元素。在这个函数中你可以来处理你的注解,我们在这里实现生成代码的逻辑
getSupportedAnnotationTypes()
返回注解处理器支持的注解类型,你可以认为这个方法的返回值是process
方法的第一个参数值。这个方法也可以使用@SupportedAnnotationTypes
注解来替代,二者等效
getSupportedSourceVersion()
指明支持代码生成的java版本。这个方法也可以使用@SupportedSourceVersion
注解来替代,二者等效
注解处理器方法实现
推荐使用JavaPoet来完成新的class文件的生成
向javac声明这个注解处理器
在处理器模块代码根目录下(一般是src/main),创建resources/META-INF/services/javax.annotation.processing.Processor文件,在这个文件中声明你的注解器类路径1
com.github.tedaliez.processor.NewIntentProcessor
换行可以声明下一个注解器的路径,这里我们只有一个。
编译,体验成果
具体的代码可以参考https://github.com/TedaLIEz/AptExample
注解处理器不能做(不建议)的事情
- 修改已有的类文件。注解处理器的初衷是希望用注解来生成一些新的类文件,修改已有的类文件可能会有办法,但属于特殊技巧
- 受1的限制,注解处理器并不能很好地实现面向切面编程,因为我们很难侵入到一个方法执行过程中加入一段我们的代码。
注解处理器的特性
注解处理器有下面几个特性:
- 提供编译期的错误检查(fail-fast).更容易暴露错误和调试,个人认为这是注解处理器解决问题的最大优势
- 提供了类似C++的模版能力,比C++的模版有着更好的错误信息
- 避开了运行期使用反射获取注解带来的性能问题