前面,介绍了 Java5.0 引入的注解,现在来介绍注解处理机制。
注解处理机制 = 注解 + 注解处理器
注解处理器和注解一般共同组成 Java Library
,它对外提供了特定的功能。
注解处理器 APT(Annotation Processing Tool),在编译期间,JVM 可以加载注解处理器来处理对应的注解,处理方式一般是根据注解信息生成代码,避免我们重复一些无聊的代码。
先来一张 Android 的 AOP (Aspect-Oriented Programming 面向切面编程) 的图来理解 APT 的作用。
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
文件添加如下依赖:
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 ,相互隔离。
注解处理器的 API 定义在 javax.annotation.processing
包中,其中 Processor
接口定义注解处理器,其子类 AbstractorProcessor
是一个抽象类,它额外添加了便捷方法,我们一般使用这个子类进行注解处理。
首先,新建一个自定义注解处理器 MyProcessor
,然后继承 AbstractorProcessor
,它有四个重要的方法:
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 getSupportedAnnotationTypes() { }
@Override //表示可以处理的 java 版本
public SourceVersion getSupportedSourceVersion() { }
}
上面有注释,每个方法都有其对应的功能。
但现在,我们一般不这么写,现在许多方法可以直接使用注解替代:
@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 文件相关的辅助类,它可以创建文件。
Elements 元素,源代码中的每一部分都是一个特定的元素类型,分别代表了包、类、方法等等。
Elementd 的每一个子类都代表一种特定类型:
下面来列举一个普通的类:
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 树,可以通过一个元素去访问它的父元素或者子元素。
element.getEnclosingElement();// 获取父元素
element.getEnclosedElements();// 获取子元素
Messager 是日志输出工具。
虽然是编译时执行 Processor,但也是可以输入日志信息用于调试的。
Processor 日志输出的位置在编译器下方的 Gradle Console 窗口中。
@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
process(Set extends TypeElement> annotations, RoundEnvironment roundEnv)
是注解处理器的核心方法,必须实现。
APT 会扫描源码中所有的相关注解,然后会回调process()
这个方法,如果没有扫描到声明的相关注解,则不会回调此方法。
它的实现一般可以分为两个步骤,首先收集信息,然后根据收集的信息生成代码。
RoundEnvironment
来获取想要的 Elements 。先介绍这个方法发两个参数Set extends TypeElement>
和 RoundEnvironment
。
将返回所有的由该Processor处理,并待处理的 Annotations。
属于该Processor处理的注解,但并未被使用,不存在与这个集合里。
表示当前或是之前的运行环境,可以通过该对象查找到需要的注解。
常使用的方式是:
for (Element element : roundEnv.getElementsAnnotatedWith(RouterService.class)) {
//取出所有被@RouterService 标记的元素,然后遍历
}
Element 中并没有子类特定的扩展方法,这时候往往需要强制类型转换。
如果能确定取出的元素类型,一般可以这样:
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 是 square 公司开源的库,他能优雅的生成代码,让你从重复无聊的代码中解放出来。
如果英文好,可以直接看 Github 上的文档。依赖方式:
compile 'com.squareup:javapoet:1.4.0'
javapoet 里面常用的几个类:
MethodSpec
代表一个构造函数或方法声明。TypeSpec
代表一个类,接口,或者枚举声明。FieldSpec
代表一个成员变量,一个字段声明。JavaFile
包含一个顶级类的 Java 文件。如果我们想生成下面的类:
package com.example.helloworld;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}
那么,我们使用 Javapoet 如下:
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
等,以上都有注释。
有时候,我们的方法有复杂的参数,那如何定义呢?比如集合
MethodSpec method = MethodSpec.methodBuilder("testList")
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(ParameterizedTypeName.get(
ClassName.get(ArrayList.class),
ClassName.get(packageName, "TestBean"))
, "beanList")
.build();
效果如下:
public void testList(ArrayList beanList){
}
以上只是简单的 ArrayList
,如果是更加复杂的 HashMap
呢?而且如果里面继续嵌套ArrayList
呢?
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();
效果如下:
public void testHashMap(HashMap hashMap){
}
可以看到,ParameterizedTypeName
可以无限循环嵌套,任何复杂的参数,都可以使用ParameterizedTypeName
自定义出来。
我们首先定义一个简单的成员变量
TypeSpec helloWorld = TypeSpec.classBuilder("test")
.addModifiers(Modifier.PUBLIC)
.addField(String.class, "name", Modifier.PRIVATE, Modifier.FINAL) //添加成员变量
.build();
结果如下:
public class test(){
private final String name;
}
这是简单的添加变量,如果还需要对变量进行赋值呢?
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();
结果如下:
public class test(){
private final String name = "Lollipop v." + 5.0;
}
如果遇到判定、循环、选择等,可以使用beginControlFlow
和endControlFlow
,就像这样
MethodSpec main = MethodSpec.methodBuilder("main")
.addStatement("int total = 0") //添加语句
.beginControlFlow("for (int i = 0; i < 10; i++)") //开始控制流,即此语句后添加大括号 {
.addStatement("total += i") //在控制流里面添加语句
.endControlFlow() //结束流,即 }
.build();
效果如下:
void main() {
int total = 0;
for (int i = 0; i < 10; i++) {
total += i;
}
}
构造方法使用 MethodSpec.constructorBuilder()
:
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();
结果如下:
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 至少会扫描两次源码。如果第二次扫描后继续生成有注解的代码,那么类似递归一样会再次扫描,直到不再出现新注解。所以,这个流程可以无限递归。
因此,在生成的代码中如果需要添加注解,一定要慎重,避免出现死循环。
注解处理器写出来后,需要IDE 来加载这些注解处理器。
刚开始,Android studio 中加载注解处理器的方式是使用第三方插件 android-apt
,但随着 Android Gradle 插件 2.2 版本发布之后, android-apt
作者在官网发表声明称,后续将不会继续维护 android-apt
,并推荐大家使用 Android 官方插件提供的相同能力。Android Gradle 插件提供了名为 annotationProcessor
的功能来完全代替 android-apt
。
现在看看 annotationProcessor
的使用方式:
首先,确保 Android 工程的 Gradle 插件版在 2.2 及以上,
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.1'
}
}
其次,使用annotationProcessor
声明注解依赖,以依赖 Dagger 2 为例:
dependencies {
compile 'com.google.dagger:dagger:2.0'
annotationProcessor 'com.google.dagger:dagger-compiler:2.0'
}
可以看到,现在使用annotationProcessor
相较于以前使用android-apt
方便了很多。