注解

注解及APT实现简易ButterKnife试验

2020-03-01  本文已影响0人  fastcv

前言

关于注解和APT(Annotation Processing Tool),是之前看到的一些文章记录过,觉得很神奇,于是决定抽空来研究研究,网上许多实现butterknife的,所以决定模仿一下来自己实现,通过这个试验来了解一下apt的作用和注解的一些基础理论知识,废话不多说,直接开始。

测试机配置及项目相关配置

理论知识

理论知识是实践的基础

先来简单讲一讲注解这个知识点。注解(Annotation)相当于一种标记(标签),可以理解为我们为相应的字段、方法或类等加上一个标签,然后程序运行时检查有什么标签,然后根据标签去做一些相应的处理。它是一种特殊的类。

举个栗子,我们常见的注解Override 的写法

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

这里就有几个知识点需要去了解:

注解类的格式

格式看起来很接口的样子差不多

public @interface 注解的名字{
}

Ex:
public @interface MyAnnotation{
}
Retention

这是java几种元注解中的一种(元注解我理解为注解在我们自己定义的注解上的注解),它总共有三个值:

那么,分别代表什么呢?其实就是表示注解保存的地方,我们知道,我们写的代码是.java文件,然后编译成.class文件,最后加载到内存中去,所对应的Retention的三个值的含义就是:

Target

这是java几种元注解中的一种,总共有如下几种值:

注解类的属性

注解类是一种特殊的类,它也可以给自己添加属性,比如:

@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调试)(实测有效)

好了,这篇文章先到这里,后期有新的的再补充(客套话)。


上一篇下一篇

猜你喜欢

热点阅读