APT(Annotation Processing Tool)即注解处理器,是一种用来处理注解的工具。JVM会在编译期就运行APT去扫描处理代码中的注解而后输出java文件。java
简单来讲~~就是你只须要添加注解,APT就能够帮你生成须要的代码git
许多的Android开源库都使用了APT技术,如ButterKnife、ARouter、EventBus等程序员
在使用Java开发Android时,页面初始化的时候咱们一般要写大量的view = findViewById(R.id.xx)代码github
做为一个优(lan)秀(duo)的程序员,咱们如今就要实现一个APT来完成这个繁琐的工做,经过一个注解就能够自动给View得到实例api
本demo地址bash
建立一个项目,名称叫 apt_demo
建立一个Activity,名称叫 MainActivity
在布局中添加一个TextView, id为test_textview
app
建立一个Java Library Module名称叫 apt-annotation
框架
在这个module中建立自定义注解 @BindView
ide
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
复制代码
@Retention(RetentionPolicy.CLASS)
:表示这个注解保留到编译期@Target(ElementType.FIELD)
:表示注解范围为类成员(构造方法、方法、成员变量)建立一个Java Library Module名称叫 apt-compiler
工具
在这个Module中添加依赖
dependencies {
implementation project(':apt-annotation')
}
复制代码
在这个Module中建立BindViewProcessor
类
直接给出代码~~
public class BindViewProcessor extends AbstractProcessor {
private Filer mFilerUtils; // 文件管理工具类
private Types mTypesUtils; // 类型处理工具类
private Elements mElementsUtils; // Element处理工具类
private Map<TypeElement, Set<ViewInfo>> mToBindMap = new HashMap<>(); //用于记录须要绑定的View的名称和对应的id
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFilerUtils = processingEnv.getFiler();
mTypesUtils = processingEnv.getTypeUtils();
mElementsUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes; //将要支持的注解放入其中
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();// 表示支持最新的Java版本
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
System.out.println("start process");
if (set != null && set.size() != 0) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//得到被BindView注解标记的element
categories(elements);//对不一样的Activity进行分类
//对不一样的Activity生成不一样的帮助类
for (TypeElement typeElement : mToBindMap.keySet()) {
String code = generateCode(typeElement); //获取要生成的帮助类中的全部代码
String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //构建要生成的帮助类的类名
//输出帮助类的java文件,在这个例子中就是MainActivity$$Autobind.java文件
//输出的文件在build->source->apt->目录下
try {
JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
Writer writer = jfo.openWriter();
writer.write(code);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
return false;
}
//将须要绑定的View按不一样Activity进行分类
private void categories(Set<? extends Element> elements) {
for (Element element : elements) { //遍历每个element
VariableElement variableElement = (VariableElement) element; //被@BindView标注的应当是变量,这里简单的强制类型转换
TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //获取表明Activity的TypeElement
Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views储存着一个Activity中将要绑定的view的信息
if (views == null) { //若是views不存在就new一个
views = new HashSet<>();
mToBindMap.put(enclosingElement, views);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class); //获取到一个变量的注解
int id = bindAnnotation.value(); //取出注解中的value值,这个值就是这个view要绑定的xml中的id
views.add(new ViewInfo(variableElement.getSimpleName().toString(), id)); //把要绑定的View的信息存进views中
}
}
//按不一样的Activity生成不一样的帮助类
private String generateCode(TypeElement typeElement) {
String rawClassName = typeElement.getSimpleName().toString(); //获取要绑定的View所在类的名称
String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //获取要绑定的View所在类的包名
String helperClassName = rawClassName + "$$Autobind"; //要生成的帮助类的名称
StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n"); //构建定义包的代码
builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //构建import类的代码
builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper"); //构建定义帮助类的代码
builder.append(" {\n"); //代码格式,能够忽略
builder.append("\t@Override\n"); //声明这个方法为重写IBindHelper中的方法
builder.append("\tpublic void inject(" + "Object" + " target ) {\n"); //构建方法的代码
for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍历每个须要绑定的view
builder.append("\t\t"); //代码格式,能够忽略
builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n"); //强制类型转换
builder.append("\t\t"); //代码格式,能够忽略
builder.append("substitute." + viewInfo.viewName).append(" = "); //构建赋值表达式
builder.append("substitute.findViewById(" + viewInfo.id + ");\n"); //构建赋值表达式
}
builder.append("\t}\n"); //代码格式,能够忽略
builder.append('\n'); //代码格式,能够忽略
builder.append("}\n"); //代码格式,能够忽略
return builder.toString();
}
//要绑定的View的信息载体
class ViewInfo {
String viewName; //view的变量名
int id; //xml中的id
public ViewInfo(String viewName, int id) {
this.viewName = viewName;
this.id = id;
}
}
}
复制代码
咱们本身实现的APT类须要继承AbstractProcessor
这个类,其中须要重写如下方法:
init(ProcessingEnvironment processingEnv)
getSupportedAnnotationTypes()
getSupportedSourceVersion()
process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)
这不是一个抽象方法,但progressingEnv参数给咱们提供了许多有用的工具,因此咱们须要重写它
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFilerUtils = processingEnv.getFiler();
mTypesUtils = processingEnv.getTypeUtils();
mElementsUtils = processingEnv.getElementUtils();
}
复制代码
mFilterUtils
文件管理工具类,在后面生成java文件时会用到mTypesUtils
类型处理工具类,本例不会用到mElementsUtils
Element处理工具类,后面获取包名时会用到限于篇幅就不展开对这几个工具的解析,读者能够自行查看文档~
由方法名咱们就能够看出这个方法是提供咱们这个APT可以处理的注解
这也不是一个抽象方法,但仍须要重写它,不然会抛出异常(滑稽),至于为何有兴趣能够自行查看源码~
这个方法有固定写法~
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes; //将要支持的注解放入其中
}
复制代码
顾名思义,就是提供咱们这个APT支持的版本号
这个方法和上面的getSupportedAnnotationTypes()相似,也不是一个抽象方法,但也须要重写,也有固定的写法
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();// 表示支持最新的Java版本
}
复制代码
最主要的方法,用来处理注解,这也是惟一的抽象方法,有两个参数
set
参数是要处理的注解的类型集合roundEnv
表示运行环境,能够经过这个参数得到被注解标注的代码块@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
System.out.println("start process");
if (set != null && set.size() != 0) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//得到被BindView注解标记的element
categories(elements);//对不一样的Activity进行分类
//对不一样的Activity生成不一样的帮助类
for (TypeElement typeElement : mToBindMap.keySet()) {
String code = generateCode(typeElement); //获取要生成的帮助类中的全部代码
String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //构建要生成的帮助类的类名
//输出帮助类的java文件,在这个例子中就是MainActivity$$Autobind.java文件
//输出的文件在build->source->apt->目录下
try {
JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
Writer writer = jfo.openWriter();
writer.write(code);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
return false;
}
复制代码
process方法的大概流程是:
@BindView
标记的Elementcategories
方法,把全部须要绑定的View变量按所在的Activity进行分类,把对应关系存在mToBindMap中generateCode
方法得到要生成的代码,每一个Activity生成一个帮助类把全部须要绑定的View变量按所在的Activity进行分类,把对应关系存在mToBindMap中
private void categories(Set<? extends Element> elements) {
for (Element element : elements) { //遍历每个element
VariableElement variableElement = (VariableElement) element; //被@BindView标注的应当是变量,这里简单的强制类型转换
TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //获取表明Activity的TypeElement
Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views储存着一个Activity中将要绑定的view的信息
if (views == null) { //若是views不存在就new一个
views = new HashSet<>();
mToBindMap.put(enclosingElement, views);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class); //获取到一个变量的注解
int id = bindAnnotation.value(); //取出注解中的value值,这个值就是这个view要绑定的xml中的id
views.add(new ViewInfo(variableElement.getSimpleName().toString(), id)); //把要绑定的View的信息存进views中
}
}
复制代码
注解应该已经很详细了~
实现方式仅供参考,读者能够有本身的实现
按照不一样的TypeElement生成不一样的帮助类(注:参数中的TypeElement对应一个Activity)
private String generateCode(TypeElement typeElement) {
String rawClassName = typeElement.getSimpleName().toString(); //获取要绑定的View所在类的名称
String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //获取要绑定的View所在类的包名
String helperClassName = rawClassName + "$$Autobind"; //要生成的帮助类的名称
StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n"); //构建定义包的代码
builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //构建import类的代码
builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper"); //构建定义帮助类的代码
builder.append(" {\n"); //代码格式,能够忽略
builder.append("\t@Override\n"); //声明这个方法为重写IBindHelper中的方法
builder.append("\tpublic void inject(" + "Object" + " target ) {\n"); //构建方法的代码
for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍历每个须要绑定的view
builder.append("\t\t"); //代码格式,能够忽略
builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n"); //强制类型转换
builder.append("\t\t"); //代码格式,能够忽略
builder.append("substitute." + viewInfo.viewName).append(" = "); //构建赋值表达式
builder.append("substitute.findViewById(" + viewInfo.id + ");\n"); //构建赋值表达式
}
builder.append("\t}\n"); //代码格式,能够忽略
builder.append('\n'); //代码格式,能够忽略
builder.append("}\n"); //代码格式,能够忽略
return builder.toString();
}
复制代码
你们能够对比生成的代码
package com.example.apt_demo;
import com.example.apt_api.template.IBindHelper;
public class MainActivity$$Autobind implements IBindHelper {
@Override
public void inject(Object target ) {
MainActivity substitute = (MainActivity)target;
substitute.testTextView = substitute.findViewById(2131165315);
}
}
复制代码
有没有以为字符串拼接很麻烦呢,不只麻烦还容易出错,那么有没有更好的办法呢(留个坑)
一样的~ 这个方法的设计仅供参考啦啦啦~~~
这应该是最简单的一步
这应该是最麻烦的一步
客官别急,往下看就知道了~
注册一个APT须要如下步骤:
正如我前面所说的~
简单是由于都是一些固定的步骤
麻烦也是由于都是一些固定的步骤
4.1 建立一个Android Library Module,名称叫apt-api
,并添加依赖
dependencies {
...
api project(':apt-annotation')
}
复制代码
4.2 分别建立launcher、template文件夹
4.3 在template文件夹中建立IBindHelper
接口
public interface IBindHelper {
void inject(Object target);
}
复制代码
这个接口主要供APT生成的帮助类实现
4.4在launcher文件夹中建立AutoBind
类
public class AutoBind {
private static AutoBind instance = null;
public AutoBind() {
}
public static AutoBind getInstance() {
if(instance == null) {
synchronized (AutoBind.class) {
if (instance == null) {
instance = new AutoBind();
}
}
}
return instance;
}
public void inject(Object target) {
String className = target.getClass().getCanonicalName();
String helperName = className + "$$Autobind";
try {
IBindHelper helper = (IBindHelper) (Class.forName(helperName).getConstructor().newInstance());
helper.inject(target);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
这个类是咱们的API的入口类,使用了单例模式
inject方法是最主要的方法,但实现很简单,就是经过反射去调用APT生成的帮助类的方法去实现View的自动绑定
在app module里添加依赖
dependencies {
...
annotationProcessor project(':apt-compiler')
implementation project(':apt-api')
}
复制代码
public class MainActivity extends AppCompatActivity {
@BindView(value = R.id.test_textview)
public TextView testTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AutoBind.getInstance().inject(this);
testTextView.setText("APT 测试");
}
}
复制代码
大功告成!咱们来运行项目试试看
TextView已经正确显示了文字,咱们的小demo到这里就完成啦~
咱们的APT还能够变得更简单!
下面咱们来逐个击破~
JavaPoet是一个用来生成Java代码的框架,对JavaPoet不了解的请移步官方文档
JavaPoet生成代码的步骤大概是这样(摘自官方文档):
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")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
复制代码
使用JavaPoet来生成代码有不少的优势,不容易出错,能够自动import等等,这里不过多介绍,有兴趣的同窗能够自行了解
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.apt_annotation.BindView")
public class BindViewProcessor extends AbstractProcessor {
...
}
复制代码
这是javax.annotation.processing中提供的注解,直接使用便可
这是谷歌官方出品的一个开源库,能够省去注册APT的步骤,只须要一行注释
先在apt-compiler模块中添加依赖
dependencies {
...
implementation 'com.google.auto.service:auto-service:1.0-rc2'
}
复制代码
而后添加注释便可
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
...
}
复制代码
APT是一个很是强大并且频繁出如今各类开源库的工具,学习APT不只可让咱们在阅读开源库源码时游刃有余也能够本身开发注解框架来帮本身写代码~ 本demo地址