annotation processor tool的套路
注解一般有用在运行时和编译时,最近了解了一些编译时注解的知识,然后把翔神的代码拷贝下来撸了一把,基本了解了整个套路,直白点说:编译时注解就是在编译时生成类(一般是java文件),替代我们手工编写java类。本篇先从宏观来解剖apt的套路,再从细节来补充原理。
项目结构
本篇就直接以翔神的代码ioc-apt-sample来作为示例,项目结构:
view-annotation
需要用到哪些注解,就写在这里,例如butterknife中的bindView,bindString,onClick等。本示例中写了一个Bind的注解。
Butterknife:
本示例:
viewinject-api
向外提供调用api,例如ButterKnife.bind(this)。本示例是ViewInjector.injectView(this) 和
ViewInjector.injectView(this,view)
viewinject-compiler
这个库干的事就是在编译的时候生成java类,这个生成的java类里面实际就是在做findViewById。这就是为啥我们不用写findViewById的原因。apt的套路就在这个库,后面我们详细说。注意view-compiler是java库。
依赖关系:
module:
compile project(‘:viewinject-api’)
apt project(‘:viewinject-compiler’)viewinject-api:
compile project(‘:viewinject-annotation’)viewinject-compiler:
compile ‘com.google.auto.service:auto-service:1.0-rc3’
compile project (‘:viewinject-annotation’)project
dependencies {
classpath ‘com.android.tools.build:gradle:2.3.2’
classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.4’//viewinject-sample model
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
过程逆向解析
整个过程分两部分来看,首先是编译时生成Java类。
1.编译时
首先我们在我们的module里面是这样的:
然后编译的时候,注意是编译时不是运行时,我们可以Rebuild Project一把,然后看到module里会生成如下java类:
这是因为viewinject-compiler里面的注解处理器会扫描module中哪些类引用Bind注解就会生成对应的实现了ViewInject接口的xx$$ViewInject类。我们看下其中一个类:
里面无非是Finder.ACTIVITY或者Finder.VIEW的findView()方法。看下Finder类里面:
其实对应的就是Activity.this.findViewById()和view.findViewById()。
2.运行时
然后我们再看ViewInjector类:
当我们在运行时,注意这是运行时了,运行时调用了ViewInjector.injectView(this)或者ViewInjector.injectView(this,view)的时候,先用反射获取一个ViewInject的实例。然后VIewInject实例调用inject()方法,其实也就调用了findViewById()这个方法。一句话,ViewInjector.injectView(this)其实相当于我们原来手写的findViewById()。
好了,到此,你应该明白了整个流程是怎么一回事了。
编译时,注解处理器扫描哪些类用了注解就生成对应的实现了ViewInject接口的xx$$ViewInject类,里面的inject()其实就是就是findViewById()。然后运行时,ViewInjector.injectView()其实用反射获取一个ViewInject实例,然后调用ViewInject的inject()方法。
注解处理器
上面整个过程理解了,我们来看一下注解处理器Processor,它是怎么在编译时生成java类的呢?我自己先在viewinject-compile加了个MyProcessor继承AbstractProcessor。这个类需要引用@AutoService(Processor.class),所以viewinject-compile中要依赖compile ‘com.google.auto.service:auto-service:1.0-rc3’。为什么要引用这个包,我们后面说。前面几个都是固定写法,注释都已经写明了。我们主要看process这个方法。
process方法里面两个参数:
set: 里面装的getSupportedAnnotationTypes()方法里面放的注解类的元素信息的集合。
roundEnvironment: roundEnvironment.getElementsAnnotatedWith(Bind.class)获取
被注解元素的信息的集合。
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
private Messager messager;
private Elements elementUtils;
/**
* processingEnvironment提供Elements,Types和Filer等生成类(.java文件)的工具
*
* @param processingEnvironment
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
messager = processingEnv.getMessager();//消息打印。
//获取一个Element的相关信息。例如elementUtils.getPackageOf(element);
elementUtils = processingEnv.getElementUtils();
}
/**
* 这个方法返回一个字符串的集合,你写的注解类都会装在这个集合里面。注解处理器会根据
* 处理集合里面所有的注解
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(Bind.class.getCanonicalName());
return annotations;
}
/**
* 指定使用的Java版本,一般我们返回SourceVersion.latestSupported()
*
* @return java版本
* public enum SourceVersion {
* RELEASE_0,
* RELEASE_1,
* RELEASE_2,
* RELEASE_3,
* RELEASE_4,
* RELEASE_5,
* RELEASE_6,
* RELEASE_7,
* RELEASE_8;
* }
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 看到set和roundEnvironment两个参数了吧
* @param set:里面装的getSupportedAnnotationTypes()方法里面放的注解类的元素信息的集合。
* @param roundEnvironment:roundEnvironment.getElementsAnnotatedWith(Bind.class)获取
* 被注解元素的信息的集合。
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
/**
* 哪些地方用了Bind注解,就返回这个Element
* Element一般有几下几种类型:
- VariableElement //一般代表成员变量
- ExecutableElement //一般代表类中的方法
- TypeElement //一般代表代表类
- PackageElement //一般代表Package
*/
for (Element element : roundEnvironment.getElementsAnnotatedWith(Bind.class)) {
System.out.println("------------------------------");
System.out.println("TypeMirror---"+element.asType());
System.out.println("TypeMirror.getKind()---"+element.asType().getKind());
System.out.println("ElementKind---"+element.getKind());
System.out.println("element---"+element);
System.out.println("element.getSimpleName()---"+element.getSimpleName());
// 判断元素的类型为Field
if (element.getKind() == ElementKind.FIELD){
VariableElement vElement = (VariableElement) element;
//System.out.println("VariableElement---"+vElement);
System.out.println(vElement.getAnnotation(Bind.class).value());
}
System.out.println("------------------------------");
}
return false;
}
}
我们先直接Rebuild Project一把,然后对应的打印结果:
下面我们重点来说下Element,然后再回头来看打印结果。
Element元素
- Element里面能拿到ElementKind,Name等一系列属性:
- ElementKind类:
- Element子类:
其实就是跟meta元素标签类似的对一个类的一种封装形式。再回头看打印结果,应该能弄清楚是怎么一回事了。示例中,我们把Bind注解用在成员上,所以elementKind是Field,Element一般有几下几种类型:
VariableElement //一般代表成员变量
ExecutableElement //一般代表类中的方法
TypeElement //一般代表代表类
PackageElement //一般代表Packageelement跟element.getSimpleName就是成员的名称
TypeMirror是这个成员的Class全称。嗯~~基本就是这样,如果你想弄清楚更多,直接一个个编译打印一次就知道了。
Messager
Messager相当于一个信息跟踪系统,跟log日志类似,我们主要用来追踪错误。
if (annotatedElement.getKind() != ElementKind.FIELD)//被注解的元素不是成员
{
error(annotatedElement, "%s must be declared on field.", clazz.getSimpleName());
return false;
}
if (ClassValidator.isPrivate(annotatedElement))//成员是私有的
{
error(annotatedElement, "%s() must can not be private.", annotatedElement.getSimpleName());
return false;
}
private void error(Element element, String message, Object... args) {
if (args.length > 0) {
message = String.format(message, args);
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);
}
//*****************************************************************************
//Diagnostic.Kind有几种类型:
public static enum Kind {
ERROR,
WARNING,
MANDATORY_WARNING,
NOTE,
OTHER;
private Kind() {
}
}
比如我们将Bind注解定义在class上,Rebuild的时候就会报如下错误:
JavaPoet和Filer
通过注解处理器来生成 .java 源文件基本上都会使用javapoet 这个库,JavaPoet一个是用于产生 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件,(当然你也可以不用这个JavaPoet库,自己手写,参考示例中ViewInjectProcessor操作)下面来看下具体使用方法。
首先我们需要引入JavaPoet库到viewinject-compiler中:compile ‘com.squareup:javapoet:1.9.0’。
MyProcess类:
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
private Filer filer;
/**
* processingEnvironment提供Elements,Types和Filer等生成类(.java文件)的工具
*
* @param processingEnvironment
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
}
/**
* 这个方法返回一个字符串的集合,你写的注解类都会装在这个集合里面。注解处理器会根据
* 处理集合里面所有的注解
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
//annotations.add(Bind.class.getCanonicalName());
annotations.add(MyAnnotation.class.getCanonicalName());
return annotations;
}
/**
* 指定使用的Java版本,一般我们返回SourceVersion.latestSupported()
*
* @return java版本
* public enum SourceVersion {
* RELEASE_0,
* RELEASE_1,
* RELEASE_2,
* RELEASE_3,
* RELEASE_4,
* RELEASE_5,
* RELEASE_6,
* RELEASE_7,
* RELEASE_8;
* }
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 看到set和roundEnvironment两个参数了吧
*
*
* @param set
* @param roundEnvironment
*
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (TypeElement element : set) {
if (element.getQualifiedName().toString().equals(MyAnnotation.class.getCanonicalName())) {
// 创建main方法
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();
// 创建HelloWorld类
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
try {
// 生成 com.example.HelloWorld.java
JavaFile javaFile = JavaFile.builder("com.example.viewinject_sample", helloWorld)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// 生成文件
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
return false;
}
}
Rebuild Project一把,生成了HelloWorld类:
@AutoService(Processor.class)
其实我们的注解器只有注册了才能在编译的时候产生作用。相当于一个登记作用。如下:
我们如果加了这句,才会有这个Processor文件,并且文件里面是我们自己的注解器。其实我们可以不需要这个auto-service库,自己手工操作登记也可以,具体流程:
1、在 viewinject-compiler 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
这样注解处理器已经可以正常工作了,跟引入auto-service库效果一样~
android-apt和annotationProcessor
我们都会在project级别的build.gradle配置这么一句:
module中配置这些:
大体来讲它有两个作用:
1.允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library
2.设置源路径,使注解处理器生成的代码能被Android Studio正确的引用
第一个作用比较好理解:但注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,而主项目添加了这个库会引入很多不必要的文件。第二个作用暂时没发现不用的话有什么弊端。
当然了,Android Studio 2.2 Gradle 插件提供了annotationProcessor 的功能来代替 android-apt . 而且android-apt 后续将不会继续维护. 所以将android-apt切换annotationProcessor吧。在module中只需配置一句:annotationProcessor project(‘:viewinject-compiler’)就可以了。
以上差不多就是整个apt的套路了。
参考以下文章:
https://race604.com/annotation-processing/
http://blog.csdn.net/github_35180164/article/details/52121038
示例源码地址:https://github.com/hymanAndroid/ioc-apt-sample