注解及APT实现简易ButterKnife试验
前言
关于注解和APT(Annotation Processing Tool),是之前看到的一些文章记录过,觉得很神奇,于是决定抽空来研究研究,网上许多实现butterknife的,所以决定模仿一下来自己实现,通过这个试验来了解一下apt的作用和注解的一些基础理论知识,废话不多说,直接开始。
测试机配置及项目相关配置
- 测试机为谷歌原生机(模拟器)
- 开发语言为java、测试代码语言为kotlin
- 代码地址:APT试验代码
理论知识
理论知识是实践的基础
先来简单讲一讲注解这个知识点。注解(Annotation)相当于一种标记(标签),可以理解为我们为相应的字段、方法或类等加上一个标签,然后程序运行时检查有什么标签,然后根据标签去做一些相应的处理。它是一种特殊的类。
举个栗子,我们常见的注解Override 的写法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这里就有几个知识点需要去了解:
- 注解类的格式
- @Retention(RetentionPolicy.SOURCE)是什么意思
- @Target(ElementType.METHOD)是什么意思
注解类的格式
格式看起来很接口的样子差不多
public @interface 注解的名字{
}
Ex:
public @interface MyAnnotation{
}
Retention
这是java几种元注解中的一种(元注解我理解为注解在我们自己定义的注解上的注解),它总共有三个值:
- RetentionPolicy.SOURCE
- RetentionPolicy.CLASS
- RetentionPolicy.RUNTIME
那么,分别代表什么呢?其实就是表示注解保存的地方,我们知道,我们写的代码是.java文件,然后编译成.class文件,最后加载到内存中去,所对应的Retention的三个值的含义就是:
- RetentionPolicy.SOURCE表示这个注解只留在.java文件中,编译成.class时就去掉了。
- RetentionPolicy.CLASS表示这个注解一直留到.class文件中,加载到内存中时就去掉了。
- RetentionPolicy.RUNTIME同理表示一直留到加载到内存中去。
Retention标注的就是该注解存活的阶段。
Target
这是java几种元注解中的一种,总共有如下几种值:
- TYPE
- FIELD
- METHOD
- PARAMETER
- CONSTRUCTOR
- LOCAL_VARIABLE
这些值代表就是类的属性、类本身、方法等成分,Target决定了一个注解可以标识到哪些成分上。
注解类的属性
注解类是一种特殊的类,它也可以给自己添加属性,比如:
@Retention(RetentionPolicy.RUNTIME)
@Target( {ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
public @interface MyAnnotation {
String value(); //定义一个基础值
}
那么,这个注解怎么用呢?我们接下来就去新建一个类去实验一下它的用法。
实践操作
实验是检验真理的唯一标准
我们新建一个注解类和一个实验类
@Retention(RetentionPolicy.RUNTIME)
@Target( {ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
public @interface MyAnnotation {
String value(); //定义一个基础值
}
@MyAnnotation(value = "HelloWorld_type")
public class TestTool {
@MyAnnotation(value = "HelloWorld_field")
public String test;
@MyAnnotation(value = "HelloWorld_method")
public void run() {
....
}
}
获取类上注解的值
我们在run方法中写入
public void run() {
MyAnnotation annotation = TestTool.class.getAnnotation(MyAnnotation.class);
System.out.println("类上注解的值:" + annotation.value());
}
public static void main(String[] args) {
TestTool tool = new TestTool();
tool.run();
}
运行结果:
类上注解的值:HelloWorld_type
获取类中属性上注解的值
public void run() {
Field field = null;
MyAnnotation annotation = null;
try {
field = TestTool.class.getField("test");
annotation = field.getAnnotation(MyAnnotation.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (annotation != null) {
System.out.println("类中属性上注解的值:" + annotation.value());
}
}
运行结果
类中属性上注解的值:HelloWorld_field
获取类中方法上注解的值
public void run() {
Method method = null;
MyAnnotation annotation = null;
try {
method = TestTool.class.getMethod("run");
annotation = method.getAnnotation(MyAnnotation.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
if (annotation != null) {
System.out.println("类中方法上注解的值:" + annotation.value());
}
}
运行结果
类中方法上注解的值:HelloWorld_method
可以看到,最后都是通过反射得到的注解值。
这里,注解类的属性有很多中选择,详细的我也不多介绍,下面举个栗子,列举一下可以使用的属性值。
列举可以使用的属性值
- 基本数据类型
- 数组
- 枚举
- 注解(其他)
举个栗子
定义一个枚举类
public enum Color {
BLACK, BLUE, GREEN, YELLOW
}
定义一个其他的注解类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface OtherAnnotation {
String value();
}
定义我们测试的注解类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MultiAnnotation {
//基本数据类型
int age();
String name();
//数组
int[] arr_int();
//枚举
Color color();
//其他注解
OtherAnnotation otherAnnotation();
}
定义测试类
public class TestTool {
@MultiAnnotation(age = 18 , name = "Hello" , arr_int = {1,2,3,4} ,
color = Color.BLUE , otherAnnotation = @OtherAnnotation(value = "World"))
public String value;
public void run() {
MultiAnnotation annotation = null;
try {
Field field = TestTool.class.getField("value");
annotation = field.getAnnotation(MultiAnnotation.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (annotation != null) {
System.out.println("age = " + annotation.age());
System.out.println("name = " + annotation.name());
System.out.println("arr_int = " + Arrays.toString(annotation.arr_int()));
System.out.println("color = " + annotation.color().name());
System.out.println("otherAnnotation value = " + annotation.otherAnnotation().value());
}
}
}
public static void main(String[] args) {
TestTool tool = new TestTool();
tool.run();
}
运行结果
age = 18
name = Hello
arr_int = [1, 2, 3, 4]
color = BLUE
otherAnnotation value = World
测试保存周期Retention
我们以上测试的注解类都是默认写的是
@Retention(RetentionPolicy.RUNTIME)
我们来测试一下,修改保存周期之后,会出现什么样的情况
RetentionPolicy.SOURCE
测试了一下,我们获取的annotation为null
RetentionPolicy.CLASS
测试了一下,我们获取的annotation为null
我们可以看到,只有设置了相应周期才能在指定的阶段获取到注解类和值。
那么,SOURCE和CLASS这两个阶段我们用来干啥呢?
主要是用来标记某些方法或者属性需要去验证检查之类的操作,还有就是我接下来要讲的apt和注解实现简易butterknife的试验了。
APT
APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,它用来在编译时扫描和处理注解。butterknife就是用它在编译期,通过注解生成我们findviewbyid的java文件。使用APT可以让我们少写很多重复的代码。
开始仿写BindView注解
首先新建一个module(Java Library),这里我取名叫作hallo_annotation,它主要是用来存放注解类的。
新建一个注解类
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int id();
}
然后新建一个module(Java Library),这里我取名叫作hallo_compiler,它主要是用apt在编译时期来处理注解的。
module内的build.gradle的配置
apply plugin: 'java-library'
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "com.google.auto.service:auto-service:1.0-rc4"//自动配置的
annotationProcessor "com.google.auto.service:auto-service:1.0-rc4" //这个在gradle5.0以上需要的
implementation 'com.squareup:javapoet:1.11.1'//方便编写代码的
implementation project(':hallo_annotation')
}
// 解决build 错误:编码GBK的不可映射字符
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
处理注解的类
@AutoService(Processor.class)
public class AnnotationCompiler extends AbstractProcessor {
//用于节点的工具
private Elements elementUtils;
//存放需要生成的类
private Map<String, ClassBuilder> classes = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elementUtils = processingEnvironment.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//处理BindView的注解
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
//这里由于BindView注解只能标注在Field上,所以直接转为VariableElement
VariableElement variableElement = (VariableElement) element;
//首先得到类节点
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
//得到类名
String className = typeElement.getSimpleName().toString() + "_BindView";
//得到包名
String packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
//得到唯一标识符 类全路径
String fullName = className + packageName;
//创建需要生成的类对象
ClassBuilder builder = classes.get(fullName);
if (builder == null) {
builder = new ClassBuilder();
classes.put(fullName, builder);
builder.className = className;
builder.packageName = packageName;
builder.mTypeElement = typeElement;
}
//得到字段值
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int id = bindAnnotation.id();
builder.idAndFdieldNames.put(id, variableElement);
}
for (String key : classes.keySet()) {
ClassBuilder builder = classes.get(key);
try {
// 生成文件
builder.buildJavaFile().writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
}
ClassBuilder
public class ClassBuilder {
//包名
public String packageName ;
//类名
public String className ;
//类节点
public TypeElement mTypeElement;
//用于存放BindView标注的Field字段 1 - 1
public Map<Integer, VariableElement> idAndFdieldNames = new HashMap<>();
//用于存放OnClick标注的Method字段 1 - n
public Map<ExecutableElement, int[]> clickMethodNameAndIds = new HashMap<>();
public JavaFile buildJavaFile() {
ClassName activity = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(activity, "activity");
for (int id : idAndFdieldNames.keySet()) {
VariableElement element = idAndFdieldNames.get(id);
String fieldName = element.getSimpleName().toString();
String fieldType = element.asType().toString();
methodBuilder.addCode("activity." + fieldName + " = " + "(" + fieldType + ")(((android.app.Activity)activity).findViewById( " + id + "));");
}
TypeSpec helloWorld = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
return JavaFile.builder(packageName, helloWorld)
.build();
}
}
我们在app模块中去添加这两个库的依赖。然后,在某个字段上面添加这个注释,rebuild一下,我们就可以在这个目录下看到生成的文件了。
这里需要注意一点,在kotlin工程中使用这个注解时需要这么使用
@JvmField
@BindView(id = R.id.annotation_tv)
var tv: TextView? = null
添加apt依赖时要这样改
java:
implementation project(path: ':hallo_annotation')
annotationProcessor project(path: ':hallo_compiler')
kotlin:
apply plugin: 'kotlin-kapt'
.....
implementation project(path: ':hallo_annotation')
kapt project(path: ':hallo_compiler')
最后新建一个module(Android Library),这里我取名叫作hallo_butterknife,它主要是用来反射我们生成的代码,然后去调用我们实现的findviewbyid的方法。新建一个类。
HalloButterKnife
public class HalloButterKnife {
public static void bind(Activity activity) {
Class clazz = activity.getClass();
try {
Class bindViewClass = Class.forName(clazz.getName() + "_BindView");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
最后,在我们的app(kotlin)模块中,添加依赖如下
implementation project(path: ':hallo_annotation')
kapt project(path: ':hallo_compiler')
implementation project(path: ':hallo_butterknife')
使用代码
@JvmField
@BindView(id = R.id.annotation_tv)
var tv: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_annotation_test)
HalloButterKnife.bind(this)
tv!!.text = "我是被修改之后的值"
}
运行一切正常。
还有一个常用的注解,就是ButterKnife的点击事件的注解,我在网上找了许久也没有找到简易实现的文章,是决定自己来,摸着石头过河。
开始仿写OnClick注解
同理,我们在hallo_annotation模块中新增一个注解类
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] ids();
}
这里模仿ButterKnife的点击注解可以传入多个id。
接下来是去解析这个注解,我们在解析类中的把这个注解加上
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
supportTypes.add(OnClick.class.getCanonicalName());
return supportTypes;
}
然后去解析它。
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
......
//处理OnClick的注解
Set<? extends Element> elements_onclick = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
for (Element element : elements_onclick) {
//这里由于OnClick注解只能标注在Method上,所以直接转为ExecutableElement
ExecutableElement executableElement = (ExecutableElement) element;
//首先得到类节点
TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
//得到类名
String className = typeElement.getSimpleName().toString() + "_BindView";
//得到包名
String packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
//得到唯一标识符 类全路径
String fullName = className + packageName;
//创建需要生成的类对象
ClassBuilder builder = classes.get(fullName);
if (builder == null) {
builder = new ClassBuilder();
classes.put(fullName, builder);
builder.className = className;
builder.packageName = packageName;
builder.mTypeElement = typeElement;
}
//得到字段值
OnClick onClick = executableElement.getAnnotation(OnClick.class);
int[] ids = onClick.ids();
builder.clickMethodNameAndIds.put(executableElement, ids);
}
....
}
最后,修改生成点击事件的代码
public JavaFile buildJavaFile() {
ClassName activity = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(activity, "activity",Modifier.FINAL);
for (int id : idAndFdieldNames.keySet()) {
VariableElement element = idAndFdieldNames.get(id);
String fieldName = element.getSimpleName().toString();
String fieldType = element.asType().toString();
methodBuilder.addCode("activity." + fieldName + " = " + "(" + fieldType + ")(((android.app.Activity)activity).findViewById( " + id + "));\n");
}
for (ExecutableElement element : clickMethodNameAndIds.keySet()) {
String methodName = element.getSimpleName().toString();
int[] ids = clickMethodNameAndIds.get(element);
for (int id : ids) {
VariableElement variableElement = idAndFdieldNames.get(id);
if ( variableElement != null) {
String fieldName = variableElement.getSimpleName().toString();
methodBuilder.addCode("activity." + fieldName + ".setOnClickListener(new android.view.View.OnClickListener() {\n" +
" @Override\n" +
" public void onClick(android.view.View v) {\n" +
" activity. " + methodName + "(v); \n" +
" }\n" +
" });");
}
}
}
TypeSpec helloWorld = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
return JavaFile.builder(packageName, helloWorld)
.build();
}
试验代码
@JvmField
@BindView(id = R.id.annotation_tv)
var tv: TextView? = null
@JvmField
@BindView(id = R.id.annotation_bt)
var bt: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_annotation_test)
HalloButterKnife.bind(this)
tv!!.text = "我是被修改之后的值"
}
@OnClick(ids = [R.id.annotation_tv,R.id.annotation_bt])
fun click(v: View) {
when (v.id) {
R.id.annotation_tv -> {
tv!!.text = "我是被修改之后的值--自己被点击"
}
R.id.annotation_bt -> {
tv!!.text = "我是被修改之后的值--按钮点击"
}
}
}
实测有效。
------------------------------------------------ 分割线 ----------------------------------------
以下是一些干货地址:
JavaPoet的基本使用
abstractProcessor debug(apt调试)(实测有效)
好了,这篇文章先到这里,后期有新的的再补充(客套话)。