APT(Annotation Processing Tool)即注解处理器,它是一种处理注解的工具,也是javac中的一个工具。
APT可以用来在编译时扫描和处理注解。
通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。在Android中有如ButterKnife、Dagger、EventBus等第三方框架,都采用了APT。
注意,获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能。
dependencies {...// 导入自定义注解implementation project(":apt-annotation")// 指定注释处理器annotationProcessor project(":apt-compiler")...
}
apt-annotation
存放自定义注解gradle配置文件如下:
apply plugin: 'java-library'dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])
}sourceCompatibility = "1.8"
targetCompatibility = "1.8"
apt-compiler
依赖 apt-annotation
、auto-service
gradle配置文件如下:
apply plugin: 'java-library'dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])implementation project(":apt-annotation")// 注册注解,并对其生成META-INF的配置信息implementation 'com.google.auto.service:auto-service:1.0-rc6'annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'// 第三方自动生成代码框架(可以通过类的调用方式来自动生成代码,简洁高效)implementation 'com.squareup:javapoet:1.10.0'
}sourceCompatibility = "1.8"
targetCompatibility = "1.8"
为什么两个模块一定要是Java Library而不是Android Library?
- 如果创建Android Library模块会发现不能找到AbstractProcessor这个类,这是因为Android平台是基于OpenJDK的,而OpenJDK中不包含APT的相关代码。因此,在使用APT时,必须在Java Library中进行。
AbstractProcessor是一个抽象类,需要通过继承它来实现我们自己的注解解释器
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {/*** 注解处理器的初始化阶段,可以通过ProcessingEnvironment来获取一些帮助我们来处理注解的工具类*/@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);}/*** 指明有哪些注解需要被扫描到,返回注解的全路径(包名+类名)*/@Overridepublic Set<String> getSupportedAnnotationTypes() {return super.getSupportedAnnotationTypes();}/*** 用来指定当前正在使用的Java版本,一般返回SourceVersion.latestSupported()表示最新的java版本即可*/@Overridepublic SourceVersion getSupportedSourceVersion() {return super.getSupportedSourceVersion();}/*** 核心方法,注解的处理和生成代码都是在这个方法中完成*/@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {return false;}
}
META-INF/services/javax.annotation.processing.Processor
文件,并且自动将该注解标记的注解处理器添加到该文件中。ProcessingEnvironment
来获取一些帮助我们来处理注解的工具类// Element操作类
Elements elementUtils = processingEnv.getElementUtils();// 类信息工具类
Types typeUtils = processingEnv.getTypeUtils();// 日志工具类
Messager messager = processingEnv.getMessager();// 文件工具类
Filer filer = environment.getFiler();
在Java中Element是一个接口,表示一个程序元素,它可以指代包、类、方法或者一个变量。Element已知的子接口有如下几种:
我们就拿一个简单的类举例,就可以很容易理解这些元素。
不同类型Element其实就是映射了Java中不同的类元素
package com.aptdemo.user; // PackageElementpublic class User { // TypeElementprivate long id; // VariableElementprivate String username; // VariableElementpublic long getId() { // ExecutableElementreturn id;}public void setId( // ExecutableElementlong id) { // VariableElementthis.id = id;}public String getUsername() { // ExecutableElementreturn username;}public void setUsername( // ExecutableElementString username) { // VariableElementthis.username = username;}
}
仿造ButterKnife中的 @BindView 注解,自动生成findViewById代码
@Target(ElementType.FIELD) // 作用于成员属性上
@Retention(RetentionPolicy.CLASS)
public @interface BindView {int value();
}
解析相关的节点信息存放到NodeInfo
中
public class NodeInfo {/*** 包路径*/private String packageName;/*** 节点所在类名称*/private String className;/*** 节点类型名称*/private String typeName;/*** 节点名称*/private String nodeName;/*** 注解的value*/private int value;public NodeInfo(String packageName, String className, String typeName,String nodeName, int value) {this.packageName = packageName;this.className = className;this.typeName = typeName;this.nodeName = nodeName;this.value = value;}public String getPackageName() {return packageName;}public String getClassName() {return className;}public String getTypeName() {return typeName;}public String getNodeName() {return nodeName;}public int getValue() {return value;}
}
编写核心的注解处理器
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {/*** Element操作类*/private Elements mElementUtils;/*** 类信息工具类*/private Types mTypeUtils;/*** 日志工具类*/private Messager mMessager;/*** 文件创建工具类*/private Filer mFiler;/*** 节点信息缓存*/private Map<String, List<NodeInfo>> mCache = new HashMap<>();/*** 注解处理器的初始化阶段,可以通过ProcessingEnvironment来获取一些帮助我们来处理注解的工具类*/@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);mElementUtils = processingEnv.getElementUtils();mTypeUtils = processingEnv.getTypeUtils();mMessager = processingEnv.getMessager();mFiler = processingEnv.getFiler();}/*** 指明有哪些注解需要被扫描到,返回注解的全路径(包名+类名)*/@Overridepublic Set<String> getSupportedAnnotationTypes() {return Collections.singleton(BindView.class.getCanonicalName());}/*** 用来指定当前正在使用的Java版本,一般返回SourceVersion.latestSupported()表示最新的java版本即可*/@Overridepublic SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}/*** 核心方法,注解的处理和生成代码都是在这个方法中完成*/@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {if (annotations == null || annotations.isEmpty()) return false;// 获取所有 @BindView 节点Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);if (elements == null || elements.isEmpty()) return false;// 遍历节点for (Element element : elements) {// 获取节点包信息String packageName = mElementUtils.getPackageOf(element).getQualifiedName().toString();// 获取节点类信息,由于 @BindView 作用于成员属性上,所以这里使用 getEnclosingElement() 获取父节点信息String className = element.getEnclosingElement().getSimpleName().toString();// 获取节点类型String typeName = element.asType().toString();// 获取节点标记的属性名称String nodeName = element.getSimpleName().toString();// 获取注解的值int value = element.getAnnotation(BindView.class).value();// 打印mMessager.printMessage(Diagnostic.Kind.NOTE, "packageName:" + packageName);mMessager.printMessage(Diagnostic.Kind.NOTE, "className:" + className);mMessager.printMessage(Diagnostic.Kind.NOTE, "typeName:" + typeName);mMessager.printMessage(Diagnostic.Kind.NOTE, "nodeName:" + nodeName);mMessager.printMessage(Diagnostic.Kind.NOTE, "value:" + value);// 缓存KEYString key = packageName + "." + className;// 缓存节点信息List<NodeInfo> nodeInfos = mCache.get(key);if (nodeInfos == null) {nodeInfos = new ArrayList<>();nodeInfos.add(new NodeInfo(packageName, className, typeName, nodeName, value));// 缓存mCache.put(key, nodeInfos);} else {nodeInfos.add(new NodeInfo(packageName, className, typeName, nodeName, value));}}// 判断临时缓存是否不为空if (!mCache.isEmpty()) {// 遍历临时缓存文件for (Map.Entry<String, List<NodeInfo>> stringListEntry : mCache.entrySet()) {try {// 创建文件createFile(stringListEntry.getValue());} catch (Exception e) {e.printStackTrace();}}}return false;}private void createFile(List<NodeInfo> infos) throws IOException {NodeInfo info = infos.get(0);String className = info.getClassName() + "$$ViewBinding";// 方法参数ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(info.getPackageName(), info.getClassName()), "target").build();// 代码块CodeBlock.Builder codeBlockBuilder = CodeBlock.builder();for (NodeInfo nodeInfo : infos) {// target.textView = (TextView) target.findViewByID(R.id.text_view);codeBlockBuilder.add("target.$L = ($L)target.findViewById($L);",nodeInfo.getNodeName(),nodeInfo.getTypeName(),nodeInfo.getValue());}// 方法MethodSpec methodSpec = MethodSpec.methodBuilder("bind").addModifiers(Modifier.PUBLIC, Modifier.STATIC).addParameter(parameterSpec).addStatement(codeBlockBuilder.build()).returns(void.class).build();// 类TypeSpec typeSpec = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC).addMethod(methodSpec).build();// 生成文件JavaFile.builder(info.getPackageName(), typeSpec).build().writeTo(mFiler);}
}
javapoet的具体使用教程请参考:javapoet官方文档
在MainActivity中使用注解 @BindView
@BindView(R.id.text_view)
TextView textView;
然后再Rebuild Project,这样就可以在build->generated->ap_generated_sources->debug->包名
路径下,看见自动生成的文件MainActivity$$ViewBinding
。
APT自动生成的文件MainActivity$$ViewBinding
代码如下:
public class MainActivity$$ViewBinding {public static void bind(MainActivity target) {target.textView = (android.widget.TextView)target.findViewById(2131165354);;}
}
这样我们就可以仿造ButterKnife,在App模块下新建一个ButterKnife类,利用反射来调用上面方法
public class ButterKnife {public static void bind(Activity target) {try {Class<?> clazz = target.getClass();// 反射获取apt生成的指定类Class<?> bindViewClass = Class.forName(clazz.getName() + "$$ViewBinding");// 获取它的方法Method method = bindViewClass.getMethod("bind", clazz);// 执行方法method.invoke(bindViewClass.newInstance(), target);} catch (Exception e) {e.printStackTrace();}}
}
这里我们的BindView注解功能就大功告成了
public class MainActivity extends AppCompatActivity {@BindView(R.id.text_view)TextView textView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ButterKnife.bind(this);textView.setText("Hello BindView");}
}
注解 - APT编译时注解处理器
Java编译时注解处理器(APT)详解
javapoet官方文档