APT(Annotation Processing Tool)
是一种注解处理工具,他对源码文件进行检测找出其中的Annotation,根据注解自动生成代码,如果想要自定义的注解处理器能够正常运行,必须通过APT工具来进行处理。也就是说APT可以根据我们制定的规则来帮我们生成java文件。
java文件结构
java源文件是一种结构体语言,分为四种元素/节点,即所有的java文件都是由他们构成的,如下
package com.tianxia.apt //PackageElement 包元素/包节点
public class Main{ // TypeElement类元素/类节点
private int x; // VariableElement 属性元素/属性节点
private void Main(){ //ExecuteableElement 方法元素/方法节点
....
}
}
节点 | 意义 |
---|---|
PackageElement | 表示一个包程序元素,提供对有关包及其成员的信息的访问 |
ExecuteableElement | 表示某个类或接口的方法、构造方法或初始化程序(静态或实例) |
VariableElement | 表示一个字段、enum常量、方法或构造方法的参数、局部变量或异常参数 |
TypeElement | 表示一个类或接口程序元素。提供对有关类型及其成员的信息访问 |
需要掌握的API
方法 | |
---|---|
getEncloseedElements() | 返回该元素直接包含的子元素 |
getEnclosingElement() | 返回包含该元素的父元素,与上一个方法相反 |
getKind() | 返回element的类型,判断是那种element |
getModifiers() | 获取修饰关键字,如 public static final等 |
getSimpleName() | 获取元素名,不带包名 |
getQualifiedName() | 获取全名,如果是类元素的话包含完整的包名路径 |
getParameters() | 获取方法的参数元素,每一个元素都是VariableElement |
getReturnType() | 获取方法元素的返回类型 |
getConstantValue() | 如果属性变量被final修饰,则可以使用该方法获取他的值 |
了解了这些基本内容我们开始撸码
新建工程ButterKnifeDemo,我们以ButterKnife这个三方库为例来学习APT的用法,然后新建3个module即annotation、complier、library。其中annotation和complier为java Library,library为Android Library
然后对他们进行一些配置
1.annotation的build.gradle
// 中文乱码问题(错误: 编码GBK的不可映射字符)
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
2.complier的build.gradle
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// As3.5 + gradle5.4.1-all
compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
// 帮助我们通过类调用的形式来生成Java代码
implementation "com.squareup:javapoet:1.10.0"
// 引入annotation,处理@BindView、@Onclick注解
implementation project(':annotation')
}
// 中文乱码问题(错误: 编码GBK的不可映射字符)
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
3.app的build.gradle
implementation project(":annotation")
implementation project(":library")
annotationProcessor project(":complier")
ok,同步一下。
在annotation module中定义两个注解
//SOURCE 注解仅在源码中保留,class文件中不存在
//CLASS 注解在源码和class文件中存在,但运行时不存在
//RUNTIME 注解在源码,class文件中存在且运行时可以通过反射机制获取
@Target({ElementType.FIELD}) //作用在属性之上
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
@Target({ElementType.METHOD}) //作用在方法之上
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
int[] value();
}
library moudle文件
/**
* 核心类
*/
public class ButterKnife {
public static void bind(Activity activity){
// 拼接类名,如:MainActivity$ViewBinder
String className = activity.getClass().getName() +"$ViewBinder";
try {
//加载上述拼接类
Class<?> viewBinderClass = Class.forName(className);
//接口 = 接口实现类
ViewBinder viewBinder = (ViewBinder) viewBinderClass.newInstance();
//调用接口方法
viewBinder.bind(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public abstract class DebouncingOnClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
//调用抽象方法
doClick(v);
}
public abstract void doClick(View view);
}
public interface ViewBinder<T> {
void bind(T target);
}
compiler module文件
先来定义一个常量类,在注解处理时会用到,需要注意的是包名路径要对应自己的工程
public class Constants {
// 注解处理器中支持的注解类型
public static final String BINDVIEW_ANNOTATION_TYPES = "com.tianshang.annotation.BindView";
public static final String ONCLICK_ANNOTATION_TYPES = "com.tianshang.annotation.OnClick";
// 布局、控件绑定实现接口
public static final String VIEWBINDER = "com.tianshang.library.ViewBinder";
public static final String CLICKLISTENER = "com.tianshang.library.DebouncingOnClickListener";
public static final String VIEW = "android.view.View";
// bind方法名
public static final String BIND_METHOD_NAME = "bind";
// bind方法的参数名target
public static final String TARGET_PARAMETER_NAME = "target";
}
以及一个工具类
/**
* 字符串、集合判空工具
*/
public final class EmptyUtils {
public static boolean isEmpty(CharSequence cs) {
return cs == null || cs.length() == 0;
}
public static boolean isEmpty(Collection<?> coll) {
return coll == null || coll.isEmpty();
}
public static boolean isEmpty(final Map<?, ?> map) {
return map == null || map.isEmpty();
}
}
接下来到了这篇文章的核心之处,处理自定义注解并编写我们所需生成java文件的模板。
我们要生成的java模板是这样的
public class MainActivity_ViewBinding implements ViewBinder<com.tianshang.butterknifedemo.MainActivity> {
public void bind(final com.tianshang.butterknifedemo.MainActivity target) {
target.tv1.findViewById(R.id.tv_1);
target.tv2.findViewById(R.id.tv_2);
target.findViewById(R.id.tv_1).setOnClickListener(new DebouncingOnClickListener() {
@Override
public void onClick(View v) {
target.click(view);
}
});
target.findViewById(R.id.tv_2).setOnClickListener(new DebouncingOnClickListener() {
@Override
public void onClick(View v) {
target.click(view);
}
});
}
}
注解处理类
//用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)
//允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({Constants.BINDVIEW_ANNOTATION_TYPES, Constants.ONCLICK_ANNOTATION_TYPES})
//指定jdk的编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//以上三行是固定写法
public class ButterKnifeProcess extends AbstractProcessor { //处理类必须继承AbstractProcessor
//操作Element工具类(类,函数,属性都是Element)
private Elements elementUtils;
//type(类信息)工具类,包含用于操作TypeMirror的工具方法
private Types typeUtils;
//Messager 用来报告错误,警告和其他提示信息
private Messager messager;
//文件生成器 类、资源,Filter用来创建新的类,class文件以及辅助文件
private Filer filer;
//key:类节点,value:被@BindView注解的属性集合
private Map<TypeElement, List<VariableElement>> tempBindViewMap = new HashMap<>();
//key:类节点,value:被@OnClick注解的方法集合
private Map<TypeElement, List<ExecutableElement>> tempOnClickMap = new HashMap<>();
/**
* 用于一些初始化的操作,通过processingEnvironment参数可以获取一些有用的工具类
*
* @param processingEnvironment
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//初始化
elementUtils = processingEnvironment.getElementUtils();
messager = processingEnvironment.getMessager();
filer = processingEnvironment.getFiler();
typeUtils = processingEnvironment.getTypeUtils();
messager.printMessage(Diagnostic.Kind.NOTE,
"注解处理器初始化完成,开始处理注解------------------------------->");
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//允许处理的注解的元素不为空(即有元素被@BindView或@OnClick标记)
if (!EmptyUtils.isEmpty(set)) {
//获取所有被@BindView注解的元素集合
Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
Set<? extends Element> onClickElements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
if (!EmptyUtils.isEmpty(bindViewElements) || !EmptyUtils.isEmpty(onClickElements)) {
//赋值临时map存储,用来存储被注解的属性集合
// 比如MainActivity中有两个控件
// @BindView(R.id.tv_1)
// TextView tv1;
// @BindView(R.id.tv_2)
// TextView tv2;
// 以这种方式存储下来
// {"MainActivity_ViewBinding":列表{R.id.tv_1,R.id.tv_2}}
// onClick同理
valueOfMap(bindViewElements, onClickElements);
// 生成类文件,如:
try {
createJavaFile();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
}
return false;
}
/**
* 这个方法实现过程按照12345的顺序实现,
* @throws IOException
*/
private void createJavaFile() throws IOException {
//判断是否有需要生成的类文件
if (!EmptyUtils.isEmpty(tempBindViewMap)) {
//获取接口的类型
TypeElement viewBinderType = elementUtils.getTypeElement(Constants.VIEWBINDER);
TypeElement clickListenerType = elementUtils.getTypeElement(Constants.CLICKLISTENER);
TypeElement viewType = elementUtils.getTypeElement(Constants.VIEW);
//从下往上写(javaPoet的技巧)
for (Map.Entry<TypeElement, List<VariableElement>> entry : tempBindViewMap.entrySet()) {
//类名(TypeElement)
ClassName className = ClassName.get(entry.getKey());
//2.实现接口泛型(implements ViewBinder<MainActivity>)
ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBinderType), ClassName.get(entry.getKey()));
//4.方法体参数(final MainActivity target)
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), //参数类型(Mainactivity)
Constants.TARGET_PARAMETER_NAME) //参数名target
.addModifiers(Modifier.FINAL)
.build();
//3.方法体: public void bind(final MainActivity target) {
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME) //bind方法
.addAnnotation(Override.class) //方法注解
.addModifiers(Modifier.PUBLIC) //方法类型
.addParameter(parameterSpec);//方法创建完成
//5.方法内容
for (VariableElement fieldElement : entry.getValue()) {
//获取属性名
String fieldName = fieldElement.getSimpleName().toString();
//获取注解的值(如:R.id.tv_1)
int annotationValue = fieldElement.getAnnotation(BindView.class).value();
// target.tv1.findViewById(R.id.tv_1);
String methodContent = "$N." + fieldName + "=$N.findViewById($L)";
//加入方法内容
methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME, Constants.TARGET_PARAMETER_NAME, annotationValue);
}
if (!EmptyUtils.isEmpty(tempOnClickMap)) {
for (Map.Entry<TypeElement, List<ExecutableElement>> entry1 : tempOnClickMap.entrySet()) {
if (className.equals(ClassName.get(entry1.getKey()))) {
for (ExecutableElement executableElement : entry1.getValue()) {
String methodName = executableElement.getSimpleName().toString();
int[] value = executableElement.getAnnotation(OnClick.class).value();
for (int i = 0; i < value.length; i++) {
// target.tv1.setOnClickListener(new DebouncingOnClickListener() {
// @Override
// public void doClick(View view) {
// target.onClick(view);
// }
// });
methodBuilder.beginControlFlow("$N.findViewById($L).setOnClickListener(new $T()",
Constants.TARGET_PARAMETER_NAME,
value[i],
ClassName.get(clickListenerType))
.beginControlFlow("public void doClick($T view)",
ClassName.get(viewType))
.addStatement("$N." + methodName + ("(view)"), Constants.TARGET_PARAMETER_NAME)
.endControlFlow()
.endControlFlow(")")
.build();
}
}
}
}
}
//1.生成必须是同包;(属性的修饰符是缺失的)
JavaFile.builder(className.packageName(), //包名
TypeSpec.classBuilder(className.simpleName() + "$ViewBinder") //类名
.addSuperinterface(typeName) //实现ViewBinder接口
.addModifiers(Modifier.PUBLIC) //类的类型为public
.addMethod(methodBuilder.build()) //方法体
.build()) //类构件完成
.build()
.writeTo(filer);
}
}
}
private void valueOfMap(Set<? extends Element> bindViewElements, Set<? extends Element> onClickElements) {
if (!EmptyUtils.isEmpty(bindViewElements)) {
for (Element element : bindViewElements) {
messager.printMessage(Diagnostic.Kind.NOTE, "@BindView >>> " + element.getSimpleName());
if (element.getKind() == ElementKind.FIELD) {
VariableElement fieldElement = (VariableElement) element;
//属性节点的上层是类节点
TypeElement enClosingElement = (TypeElement) element.getEnclosingElement();
if (tempBindViewMap.containsKey(enClosingElement)) {
tempBindViewMap.get(enClosingElement).add(fieldElement);
} else {
List<VariableElement> fields = new ArrayList<>();
fields.add(fieldElement);
tempBindViewMap.put(enClosingElement, fields);
}
}
}
}
if (!EmptyUtils.isEmpty(onClickElements)) {
for (Element element : onClickElements) {
messager.printMessage(Diagnostic.Kind.NOTE, "@OnClick >>> " + element.getSimpleName());
if (element.getKind() == ElementKind.METHOD) {
ExecutableElement executableElement = (ExecutableElement) element;
//属性节点的上层是类节点
TypeElement enClosingElement = (TypeElement) element.getEnclosingElement();
if (tempOnClickMap.containsKey(enClosingElement)) {
tempOnClickMap.get(enClosingElement).add(executableElement);
} else {
List<ExecutableElement> executes = new ArrayList<>();
executes.add(executableElement);
tempOnClickMap.put(enClosingElement, executes);
}
}
}
}
}
}
最后在MainActivity中调用
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_1)
TextView tv1;
@BindView(R.id.tv_2)
TextView tv2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
tv1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
@OnClick({R.id.tv_1,R.id.tv_2})
void onClick(View view){
Toast.makeText(MainActivity.this,"dianji",Toast.LENGTH_LONG).show();
}
}
代码撸完后,make project,不出意外的话根据模板生成的java文件会出现在这个路径
总的来说APT的思路还是很清晰的写法比较固定,难点在于javapoet的api使用上,大家可以查看javapoet官网,掌握的秘诀就是多写多练,毕竟java文件结构还是很固定的。
最后是这个demo的地址,大家给赏个star吧。