注解入坑笔记:关于注解使用必须了解的——Annotation、AbstraceProcessor、APT
一.前言:
了解一个东西通常是因为它有用,我主要是为了了解现在的一些主流框架(如butterknife)的实现原理才关注Annotation,所以这篇文章是记录我在实现注解内容获取时遇到的问题。因此看这篇文章的朋友首先你需要了解Annotation的基础知识,包括什么是注解,元注解,自定义注解。靠,什么都知道了,还看这篇文章作甚?这篇文章主要是记录什么问题的呢?ok,看一下下面这段代码:
class ExampleActivity extends Activity {
@BindView(R.id.user)
EditText username;
@BindView(R.id.pass)
EditText password;
...
}
用过butterknife 的想必都不会陌生,而了解butterknife 的想必也都知道这是注解在框架中的应用。@BindView(R.id.user)这一句帮我们实现了findViewById()的功能,具体怎么实现的不是今天的内容,但是可以肯定的是一定要获取到括号里的内容,这就是今天要说的,即注解的获取。
注解的保留策略有三种:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindId {
int value();
}
2.定义一个类使用该注解
public class User {
@BindId(101)
private int viewId1;
@BindId(102)
private int viewId2;
@BindId(103)
private int viewId3;
@BindId(104)
private int viewId4;
}
3.获取注解内容
public class TestA {
public static void main(String[] s){
Class use= null;
try {
use = Class.forName("com.example.User");
//通过反射获取变量的@BindId注解
for (Field f : use.getDeclaredFields()) {
BindId bi=f.getAnnotation(BindId.class);
if(bi!=null){
System.out.println(f.getName()+"......."+bi.value());
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
4.打印内容如下:
viewId1.......101
viewId2.......102
viewId3.......103
viewId4.......104
Process finished with exit code 0
二.主题
今天的主题就是编译时注解的处理,讲之前先要说明一下网上有很多关于注解处理器的文章,一搜AbstraceProcessor,哇好多文章。既然这样为什么还要写这篇博客记录呢?一是因为网上搜到文章大多比较早,很多都是17年以前的文章;再者就是文章太多了,是的,太多了,具体体验如下:
我点开第一篇文章看到了如下配置:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
额,没看懂,好吧,点开第二篇,这篇也许容易些,然后看到了如下配置:
annotationProcessor 'com.google.dagger:dagger-compiler:2.0'
咦,怎么这个还能和前面的不一样了?再打开一个,配置如下:
compile'com.squareup:javapoet:1.8.0'
compile'com.google.auto.service:auto-service:1.0-rc2'
我。。。。。。WTF,这都是什么啊?就不能解释一下吗,该用什么。
以上就是写这篇文章的原因。
-------------------------------------------割------------------------------------------
首先了解以下几个概念
一.什么是注解处理器?
注解处理器是(Annotation Processor)是javac的一个工具,用来在编译时扫描和编译和处理注解(Annotation)。你可以自己定义注解和注解处理器去搞一些事情。一个注解处理器它以Java代码或者(编译过的字节码)作为输入,生成文件(通常是java文件)。这些生成的java文件不能修改,并且会同其手动编写的java代码一样会被javac编译。看到这里加上之前理解,应该明白大概的过程了,就是把标记了注解的类,变量等作为输入内容,经过注解处理器处理,生成想要生成的java代码。
对我来讲Annotation Process的实质用处就是在编译时通过注解获取相关数据
处理器的写法有固定的套路,继承AbstractProcessor
二.什么是APT?
APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
三.接下来开始实现一个编译处理器,具体步骤
1.自定义注解
2.注解处理器
3.处理器注册
其中1不用多说,从2说起:
上面提到了处理器有固定的套路,我们处理注解,只需要继承AbstractProcessor
package com.example;
@SupportedAnnotationTypes("com.example.BindId")//我们要处理的注解
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
//这里我们可以把得到的注解的内容进行编辑(打印或者编辑成java文件),关于参数自己查看
return true;
}
}
恩,大概就是这样,流程还是需要简单些才明了。处理器定义完了,需要注册一下,步骤3:在resources资源文件夹下新建META-INF/services/javax.annotation.processing.Processor(这个是固定的,以文件形式创建),目录结构如下:
├─MyProcessor
│ │
│ └─src
│ └─main
│ ├─java
│ │ └─com
│ │ └─example
│ │ MyProcessor.java
│ │ TestAnnotation.java
│ │
│ └─resources
│ └─META-INF
│ └─services
│ javax.annotation.processing.Processor
创建好后,把我们2中定义的处理器添加进去即可:com.example.MyProcessor
com.example.MyProcessor1
com.example.MyProcessor2
(.......有多少添加多少)
步骤只有这些,不要多想,接下来看一个例子:
首先上图看一下结构:
可以看到正是上面的三步以下是代码
BindId.java
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindId {
int value();
}
MyProcessor.java
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.annotationdemo.BindId")
public class MyProcessor extends AbstractProcessor {
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mMessager = processingEnvironment.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> bindIdElements = roundEnvironment.getElementsAnnotatedWith(BindId.class);
for (Element element : bindIdElements) {
//1.获取注解的成员变量名
VariableElement bindViewElement = (VariableElement) element;
String bindViewFiledName = bindViewElement.getSimpleName().toString();
//2.获取注解元数据
BindId bindView = element.getAnnotation(BindId.class);
int id = bindView.value();
note(String.format("成员变量名 %s = 注解元数据 %d", bindViewFiledName, id));
}
return true;
}
private void note(String msg) {
mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
}
}
这里说明一下,为了把过程简单话,这里只是在process中简单的打印一**解内容,只要有了数据做什么都容易了,不管是写java文件也好,class文件也好,自己在这里编辑就是了,对吧?以上就是对注解处理器的处理了。
四.使用
到了这里,我们上面介绍的东西还有个没用上的APT。他的作用就体现在使用上。我们新建一个Module,然后就像使用butterknife 一样使用我们定义的注解处理器,使用如下:
MainActivity.java
public class MainActivity extends AppCompatActivity {
@BindId(0x0000)
TextView t1;
@BindId(0x0001)
TextView t2;
@BindId(0x0002)
TextView t3;
@BindId(0x0003)
TextView t4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
我们自定义的注解处理器,写了这么多总要用啊,so打开我们module 的build.gradle
annotationProcessor 'com.google.dagger:dagger-compiler:2.0'//android自带的APT工具
implementation project(path: ':annotationdemo') //依赖我们上面写的的项目
这时候rebuild project通过Gradle console打印如下:
注: 成员变量名 t1 = 注解元数据 0
注: 成员变量名 t2 = 注解元数据 1
注: 成员变量名 t3 = 注解元数据 2
注: 成员变量名 t4 = 注解元数据 3
至此,我们注解的内容就获取到了
五.扩展
一.关于process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 这个方法
我们上面只用到了roundEnvironment这个参数,关于TypeElement这简单介绍一下。
@SupportedAnnotationTypes("com.example.annotationdemo.BindId")
public class MyProcessor extends AbstractProcessor {
}
关于这里这个@SupportedAnnotationTypes,它的value实际上是一个String数组,如图:
也就是说我们在一个注解处理器中可以处理多个自定义的注解,例如:
@SupportedAnnotationTypes({"com.example.annotationdemo.BindId","com.example.annotationdemo.BindId1","com.example.annotationdemo.BindId2"......})
StringBuilder sb=new StringBuilder();
for(TypeElement typeElement :set){
sb.append(typeElement.getSimpleName()+"--------");
sb.append(typeElement.getQualifiedName()+"--------");
}
note(sb.toString());
将上面代码添加到process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 这个方法打印一下可以看到:
注: BindId--------com.example.annotationdemo.BindId--------BindId1--------com.example.annotationdemo.BindId1--------......
也就是我们只要通过roundEnvironment.getElementsAnnotatedWith(typeElement);就可以获取使用该注解的所有元素集合
二.一般框架中不可能只做打印工作,而更多的是将获取的内容写入java文件,和项目一起编译,上个例子:
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.annotationdemo.BindId")
public class MyProcessor extends AbstractProcessor {
private Filer mFiler;
private Messager mMessager;
private Elements mElementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFiler = processingEnvironment.getFiler();
mMessager = processingEnvironment.getMessager();
mElementUtils = processingEnvironment.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> bindIdElements = roundEnvironment.getElementsAnnotatedWith(BindId.class);
StringBuilder sb = new StringBuilder();
for (Element element : bindIdElements) {
//1.包名
PackageElement packageElement = mElementUtils.getPackageOf(element);
String pkName = packageElement.getQualifiedName().toString();
//包装类类型
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String enclosingName = enclosingElement.getQualifiedName().toString();
VariableElement bindViewElement = (VariableElement) element;
//注解变量名
String bindViewFiledName = bindViewElement.getSimpleName().toString();
//注解的变量类型
String bindViewFiledClassType = bindViewElement.asType().toString();
//获取注解元数据
BindId bindView = element.getAnnotation(BindId.class);
int id = bindView.value();
sb.append(bindViewFiledClassType);
sb.append("___");
sb.append(bindViewFiledName);
sb.append("___");
sb.append(id);
sb.append("|||||");
note(sb.toString());//编译期间在Gradle console可查看打印信息
}
//生成文件
saveFile("com.example.annotationdemo", sb.toString());
return true;
}
private void saveFile(String pkNameQ, String content) {
String pkName = pkNameQ;
try {
JavaFileObject jfo = mFiler.createSourceFile(pkName + ".ViewBindId", new Element[]{});
Writer writer = jfo.openWriter();
writer.write(writeCode(pkName, content));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private String writeCode(String pkName, String content) {
StringBuilder builder = new StringBuilder();
builder.append("package " + pkName + ";\n\n");
builder.append("public class ViewBindId { \n\n");
builder.append("public static void main(String[] args){ \n");
builder.append("System.out.println(\"" + content + "\");\n");
builder.append("}\n");
builder.append("}");
return builder.toString();
}
private void note(String msg) {
mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
}
private void note(String format, Object... args) {
mMessager.printMessage(Diagnostic.Kind.NOTE, String.format(format, args));
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
}
这次当我们再次编译完成后可以看到在应用下的build文件夹下生成了相应的java类,如图:
该类会随着其它java源文件一起编译。
三.关于我开始提到我自己遇到的一些依赖问题,现在一一解答
a.关于classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'和 annotationProcessor 'com.google.dagger:dagger-compiler:2.0'
通过上面的例子可以知道这两个其实就是apt的工具,在早期的处理编译注解时一般都是使用的android—apt这个库;gradle2.2.X(不记得具体了)之后google出了annotationProcessor可以直接使用,不用再去配置buildscript 等东西了。我现在用的android studio3.0下的gradle:3.0.1 下编译都不需要添加annotationProcessor 'com.google.dagger:dagger-compiler:2.0'
b.关于 compile'com.google.auto.service:auto-service:1.0-rc2'这个库
从上面的过程中可以看出有两个地方处理比较麻烦,其一是处理器注册服务。而这个库就是帮助我们注册服务的,有了这个库,我们只需要这样:
@AutoService(Processor.class)
public class MyAnnotationProcessor extends AbstractProcessor {
}
一个注解就搞定啦,哈哈 c.compile'com.squareup:javapoet:1.8.0'
还有个麻烦的地方就是生成java文件时编写文件的过程,可以看到我们例子中都是使用append一行一行实现的。很麻烦,而javapoet就是为了方便这一步而引入的,具体网上搜索它的用法
四.上面项目中我们是在同一个工程下使用了依赖的方式引用的,如果是不同工程使用jar包的方式怎么处理呢?如图:
将jar包放入需要的moudle的libs目录下,配置:
compile files('libs/annotationdemo.jar')
不过现在都改用implementation 替代compile了
、