分析Android中的注解处理器

最近在更新android gradle插件到v3.1之后,使用注解处理器遇到了编译失败的情况,log日志如下:

1
2
3
4
5
6
* What went wrong:
Execution failed for task ':app:javaPreCompileCommonLeadingsigningdebug'.
> Annotation processors must be explicitly declared now. The following dependencies on the compile classpath are found to contain annotation processor. Please add them to the annotationProcessor configuration.
- letv-auto-load-annotation.jar (letv-auto-load-annotation.jar)
Alternatively, set android.defaultConfig.javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = true to continue with previous behavior. Note that this option is deprecated and will be removed in the future.
See https://developer.android.com/r/tools/annotation-processor-error-message.html for more details.

log日志中也给出了明确是原因,在gradle插件升级到3.0之后,需要显式的将注解处理器添加到编译路径中,添加依赖的关键字不再试compile,而是使用annotationProcessor。怎么解决编译失败的问题?

log日志中给出了一种解决方案,手动将注解处理器添加到编译路径,如下:

1
2
3
4
5
6
7
8
9
10
11
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
includeCompileClasspath true
}
}
}
}

这种方法官方是不推荐使用的,未来可能在gradle新版本中的插件中移除该属性,那有什么一劳永逸的方法呢?

注解处理器

我们平时开发中,经常会需要写很多重复冗余的模板代码,比如Android开发中经常写大量的findViewById,于是ButterKnife应运而生,ButterKnife的核心原理就是使用了注解处理器在编译时期帮我们生成了这些模板代码。

怎么实现自定义注解处理器?

关于注解,Java注解,这里说一下怎样实现一个注解处理器,通常需要完成以下两个步骤:

  1. 实现processor接口,重写process方法;
  2. 注册注解处理器。

实现Processor接口

创建两个Java Library,CustomAnnotation和CustomAnnotationProcessor,一个作为自定义注解的module,另一个用来存放自定义注解的处理器。其中,在CustomAnnotationProcessor添加对CustomAnnotation的依赖,项目结构如下:
项目结构

为什么需要创建两个module?这是因为注解处理器是用来帮助我们生产需要的模板代码,我们需要的是最后生成的文件,最终打包进apk中的也是生成的文件,注解处理器本身是不需要打包进apk的,所以将其放在单独的module中。

TestProcessor通过继承AbstractProcessor,实现process()方法,在process()方法中完成我们需要实现的业务逻辑。在AbstractProcessor中有两个方法是需要我们去重写的。getSupportedSourceVersion()方法中,指定注解处理器支持的Java版本,getSupportedAnnotationTypes()方法中,指定注解处理器支持的注解类型,通常在该方法中指定我们自定义的注解。

1
2
3
4
5
6
7
8
9
10
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
//LetvClassAutoLoad是自定义的注解
return Collections.singleton(LetvClassAutoLoad.class.getCanonicalName());
}

AbstractProcessor分析

自定义注解处理器,通常继承AbstractProcessor,而很少去实现Processor接口,AbstractProcessor已经为我们做了一些必要的初始化操作。在AbstractProcessor中有几个重要的方法:

methods description
init(ProcessingEnvironment processingEnv) 该方法有注解处理器自动调用,其中ProcessingEnvironment类提供了很多有用的工具类:Filter,Types,Elements,Messager等
getSupportedSourceVersion() 该方法用来指定支持的java版本,一般来说我们都是支持到最新版本,因此直接返回SourceVersion.latestSupported()即可
getSupportedAnnotationTypes() 该方法返回字符串的集合表示该处理器用于处理那些注解 ,该方法的返回类型是一个String类型的Set集合,Set集合中每个元素应该是一个注解的完整全名(包名跟类名)
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 这个方法实现了真正的注解处理生成代码的逻辑。该方法在处理的过程中可能会被调用好几次。该方法包含两参数,annotations和roundEnv,annotations是需要被处理的注解集合,roundEnv是Java提供的一个实现了RoundEnvironment接口的类的对象,该对象用于查找出程序元素上使用的注解

在AbstractProcessor中有两个关键的接口ProcessingEnvironment和RoundEnvironment,先来看看RoundEnvironment的源码:

1
2
3
4
5
6
7
8
9
10
11
12
public interface RoundEnvironment {
boolean processingOver();

//上一轮注解处理器是否产生错误
boolean errorRaised();
//返回上一轮注解处理器生成的根元素
Set<? extends Element> getRootElements();
//返回包含指定注解类型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
//返回包含指定注解类型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}

再来看一下ProcessingEnvironment源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface ProcessingEnvironment {
Map<String,String> getOptions();

//Messager用来报告错误,警告和其他提示信息
Messager getMessager();
//Filer用来创建新的源文件,class文件以及辅助文件
Filer getFiler();
//Elements中包含用于操作Element的工具方法
Elements getElementUtils();
//Types中包含用于操作TypeMirror的工具方法
Types getTypeUtils();

SourceVersion getSourceVersion();
Locale getLocale();
}

可以看到ProcessingEnvironment中涉及到几个接口,Messager,Filer,Element和Types。其中Messager用于输出一些提示信息,下面来看一下Filer,Elements和Types。

Filer

Filer用于注解处理器中创建新文件,实际开发中很少直接使用Filer来创建文件,而是使用JavaPoet,JavaPoet是Square开源的一个用来生成.java源码的库,它提供了非常丰富的API。

1
implementation 'com.squareup:javapoet:1.8.0'

比如使用JavaPoet创建如下的HelloWorld.java:

1
2
3
4
5
6
7
package top.sguotao.annotation.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
24
25
26
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//MethodSpec这个类是要引入'com.squareup:javapoet:1.8.0'包,方便通过代码创建java文件
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")
//netstat -an | grep .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();

JavaFile javaFile = JavaFile.builder("top.sguotao.annotation.helloworld", helloWorld)
.build();

try {
//这里的输出要在Gradle Console中看
javaFile.writeTo(System.out);
} catch (IOException e) {
e.printStackTrace();
}
return false;
}

其中MethodSpec对应Java中的方法或是构造函数,TypeSpec对应类,接口或是枚举。除此之外JavaPoet还提供了FieldSpec来对应属性,AnnotationSpec对应注解,ParameterSpec对应参数。JavaFile对应一个包含顶层类的Java源文件。

Element

Element表示一个静态的,语言级别的构件。而任何一个结构化文档都可以看作是由不同的element组成的结构体,比如XML,JSON等。这里我们用XML来示例:

1
2
3
4
5
<root>
<child>
<subchild>.....</subchild>
</child>
</root>

这段xml中包含了三个元素:,,。对于java源文件来说,他同样是一种结构化文档:

1
2
3
4
5
6
7
8
9
10
package top.sguotao.annotation; //PackageElement

public class HelloWorld { //TypeElement
private int x; //VariableElement
HelloWorld() {//ExecuteableElement
}

private void print(String msg) { //其中的参数msg为TypeElement
}
}

其中PackageElement,TypeElement,VariableElement和ExecuteableElement都继承自Element。

interfaces description
PackageElement 包元素
TypeElement 类或接口元素
VariableElement 字段, 枚举常量, 方法或者构造方法的参数, 局部变量及异常参数等元素等
ExecutableElement 代码方法,构造函数,类或接口的初始化代码块等元素,也包括注解类型元素

Types

在Types接口中,涉及几个比较关键的接口,分别是DeclaredType,TypeElement和TypeMirror,其中TypeElement在上面已经提到,表示类或者接口元素。

interfaces descriptioin
DeclaredType 表示声明类型:类类型还是接口类型,当然也包括参数化类型,比如Set,也包括原始类型
TypeMirror 表示java语言中的类型.Types包括基本类型,声明类型(类类型和接口类型),数组,类型变量和空类型。也代表通配类型参数,可执行文件的签名和返回类型等。TypeMirror类中最重要的是getKind()方法,该方法返回TypeKind类型。

简单来说,Element代表源代码,TypeElement代表的是源码中的类型元素,比如类。虽然我们可以从TypeElement中获取类名,TypeElement中不包含类本身的信息,比如它的父类,要想获取这信息需要借助TypeMirror,可以通过Element中的asType()获取元素对应的TypeMirror。

最后给出前面介绍这些接口的类图,说明这些类之间的相互调用关系。
20180913153683172912523.png

注册注解处理器

注册注解处理器包括以下几个步骤:

  1. 在注解处理器module的 main 目录下新建 resources 资源文件夹;
  2. 在 resources文件夹下建立 META-INF/services 目录文件夹;
  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
    2018091315368332083917.png

上面这种注册的方式太麻烦了,Google帮我们写了一个注解处理器来生成这个文件。在build.gradle中添加以下依赖,同时删除上面4步的操作,我们同样可以得到相同的结果。

1
implementation 'com.google.auto.service:auto-service:1.0-rc2'

然后在我们自己的processor上增加一个注解如下:
20180913153683382283577.png

最后我们自定义的注解处理器的运行效果如下:
2018091315368338517329.png

注解处理器的执行过程

注解处理过程可能会多于一次。官方javadoc定义处理过程如下:

注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。第一次循环的输入是运行此工具的初始输入。这些初始输入,可以看成是虚拟的第0次的循环的输出。

一个处理循环是调用一个注解处理器的process()方法。为什么process会被执行多次?原因是因为使用注解处理器新生成的文件中也可能包含我们自定义的注解,而他们也会被注解处理器处理。

分离注解处理器和注解

如我们的demo展示的那样,我们将注解和注解处理器分为了两个单独的module。为什么要这样处理呢?原因是注解处理器模块只在编译期间起作用,没有必要打包到我们的最终项目中。比如在Android项目中,存在65k个方法的限制(即在一个.dex文件中,只能寻址65000个方法),如果我们在注解处理器的module中使用了guava,并且把注解和处理器打包在一个包中,这样的话,Android APK安装包中不只是包含注解处理器的代码,而也包含了整个guava的代码。Guava有大约20000个方法。所以分开注解和处理器是非常有意义的。

Android-Apt 和 AnnotationProcessor

APT是集成在javac当中的工具,Anroid-apt是用在Android Studio中处理注解处理的插件。它有两方面的作用:

  1. 只允许配置编译时注解处理器依赖,但在最终APK或者Library中不包含注解处理器的代码。
  2. 这个插件可以自动的帮你为生成的代码创建目录,使注解处理器生成的代码能被Android Studio正确的引用,让生成的代码编译到APK里面去。

对与在一个jar包中的注解处理器(API和处理器)而言,我们不需要进行特殊的配置,它照样可以工作。如果我们需要在项目当中引用注解处理器生成的代码,那么就需要使用Android-apt插件来帮助解决。

在Android Gradle 插件 2.2 版本的发布前,使用的是 android-apt 插件,但是发布后,该作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。也就是说,大约三年前推出的 android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。android-apt只支持javac编译器相比,annotationProcessor同时支持javac和jack编译器。

使用android-apt需要在project的最外层的build.gradle中添加以下依赖:

1
2
3
4
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}

使用annotationProcessor的方式

1
2
3
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
}

在需要使用注解处理器module的build.gradle文件中,使用android-apt的方式时,添加以下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
compile 'org.greenrobot:eventbus:3.0.0'
apt'org.greenrobot:eventbus-annotation-processor:3.0.1'//apt
}

使用annotationProcessor的方式,保留dependencies 里面的引用并且把apt 换成annotationProcessor就可以了

1
2
3
4
dependencies {
compile 'org.greenrobot:eventbus:3.0.0'
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.0.1'
}

参数的配置

使用android-apt方式在build.gradle配置参数:

1
2
3
4
5
apt  {
arguments {
eventBusIndex "org.greenrobot.eventbusperf.MyEventBusIndex"
}
}

使用annotationProcessor方式,配置参数:

1
2
3
4
5
6
7
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [ eventBusIndex : 'org.greenrobot.eventbusperf.MyEventBusIndex' ]
}
}
}

配置文件中配置的参数,可以在注解处理器的init()方法中获取:

1
2
3
4
5
6
public synchronized void init(ProcessingEnvironment processingEnv) {
Map<String, String> options = processingEnv.getOptions();
if (MapUtils.isNotEmpty(options)) {
moduleName = options.get('eventBusIndex');
}
}

调试注解处理器

由于注解处理器的代码都在编译期执行的,通常的debug调试断点是无法进入的,那么注解处理器该如何进行Debug调试呢?

编译时注解处理器是运行在一个单独的JVM当中,因此想要对它进行调试需要使用Remote Debug。注解处理器的Remote Debug调试需要完成以下几个步骤:

  • 在gradle.properties文件中添加如下的配置信息:
1
2
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
  • 开启JVM的远程调试功能,即在启动JVM的过程中添加如下的配置信息,其中address指定端口号:
1
2
3
4
5
//jdk 1.5以前写法,当然该命令是先后兼容的
-J-Xdebug -J-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005

//jdk 1.5及以后版本写法
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
  • 在编译器Android Studio中建立Remote Debugger;
    20180917153716643896390.png
  • 设置断点,重新构建工程(建议使用命令行的构建方式,提高debug的效率)
    20180917153716941364261.gif

最后给出示例Demo的地址

参考文献

编译时注解处理方
拓展篇:注解处理器最佳实践
Android注解处理初探:使用注解处理器消除样板代码
自定义Java注解处理器
AbstractProcessor介绍


评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×