annotation processor tool(apt)的套路

annotation processor tool的套路

注解一般有用在运行时编译时,最近了解了一些编译时注解的知识,然后把翔神的代码拷贝下来撸了一把,基本了解了整个套路,直白点说:编译时注解就是在编译时生成类(一般是java文件),替代我们手工编写java类。本篇先从宏观来解剖apt的套路,再从细节来补充原理。

项目结构

本篇就直接以翔神的代码ioc-apt-sample来作为示例,项目结构:
这里写图片描述

view-annotation

需要用到哪些注解,就写在这里,例如butterknife中的bindView,bindString,onClick等。本示例中写了一个Bind的注解。

Butterknife:
这里写图片描述
本示例:
这里写图片描述

viewinject-api

向外提供调用api,例如ButterKnife.bind(this)。本示例是ViewInjector.injectView(this) 和
ViewInjector.injectView(this,view)

viewinject-compiler

这个库干的事就是在编译的时候生成java类,这个生成的java类里面实际就是在做findViewById。这就是为啥我们不用写findViewById的原因。apt的套路就在这个库,后面我们详细说。注意view-compiler是java库。

  • 依赖关系

    • module
      compile project(‘:viewinject-api’)
      apt project(‘:viewinject-compiler’)

    • viewinject-api
      compile project(‘:viewinject-annotation’)

    • viewinject-compiler
      compile ‘com.google.auto.service:auto-service:1.0-rc3’
      compile project (‘:viewinject-annotation’)

    • project
      dependencies {
      classpath ‘com.android.tools.build:gradle:2.3.2’
      classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.4’//viewinject-sample model
      // NOTE: Do not place your application dependencies here; they belong
      // in the individual module build.gradle files
      }

过程逆向解析

整个过程分两部分来看,首先是编译时生成Java类。

1.编译时

首先我们在我们的module里面是这样的:

这里写图片描述

然后编译的时候,注意是编译时不是运行时,我们可以Rebuild Project一把,然后看到module里会生成如下java类:

这里写图片描述

这是因为viewinject-compiler里面的注解处理器会扫描module中哪些类引用Bind注解就会生成对应的实现了ViewInject接口的xx$$ViewInject类。我们看下其中一个类:

这里写图片描述

里面无非是Finder.ACTIVITY或者Finder.VIEW的findView()方法。看下Finder类里面:

这里写图片描述

其实对应的就是Activity.this.findViewById()和view.findViewById()。

2.运行时

然后我们再看ViewInjector类:
这里写图片描述

当我们在运行时,注意这是运行时了,运行时调用了ViewInjector.injectView(this)或者ViewInjector.injectView(this,view)的时候,先用反射获取一个ViewInject的实例。然后VIewInject实例调用inject()方法,其实也就调用了findViewById()这个方法。一句话,ViewInjector.injectView(this)其实相当于我们原来手写的findViewById()。

好了,到此,你应该明白了整个流程是怎么一回事了。

编译时,注解处理器扫描哪些类用了注解就生成对应的实现了ViewInject接口的xx$$ViewInject类,里面的inject()其实就是就是findViewById()。然后运行时,ViewInjector.injectView()其实用反射获取一个ViewInject实例,然后调用ViewInject的inject()方法。

注解处理器

上面整个过程理解了,我们来看一下注解处理器Processor,它是怎么在编译时生成java类的呢?我自己先在viewinject-compile加了个MyProcessor继承AbstractProcessor。这个类需要引用@AutoService(Processor.class),所以viewinject-compile中要依赖compile ‘com.google.auto.service:auto-service:1.0-rc3’。为什么要引用这个包,我们后面说。前面几个都是固定写法,注释都已经写明了。我们主要看process这个方法。

process方法里面两个参数:
set: 里面装的getSupportedAnnotationTypes()方法里面放的注解类的元素信息的集合。
roundEnvironment: roundEnvironment.getElementsAnnotatedWith(Bind.class)获取
被注解元素的信息的集合。

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    private Messager messager;
    private Elements elementUtils;
    /**
     * processingEnvironment提供Elements,Types和Filer等生成类(.java文件)的工具
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnv.getMessager();//消息打印。
        //获取一个Element的相关信息。例如elementUtils.getPackageOf(element);
        elementUtils = processingEnv.getElementUtils();
    }

    /**
     * 这个方法返回一个字符串的集合,你写的注解类都会装在这个集合里面。注解处理器会根据
     * 处理集合里面所有的注解
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Bind.class.getCanonicalName());
        return annotations;
    }

    /**
     * 指定使用的Java版本,一般我们返回SourceVersion.latestSupported()
     *
     * @return java版本
     * public enum SourceVersion {
     * RELEASE_0,
     * RELEASE_1,
     * RELEASE_2,
     * RELEASE_3,
     * RELEASE_4,
     * RELEASE_5,
     * RELEASE_6,
     * RELEASE_7,
     * RELEASE_8;
     * }
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 看到set和roundEnvironment两个参数了吧
     * @param set:里面装的getSupportedAnnotationTypes()方法里面放的注解类的元素信息的集合。
     * @param roundEnvironment:roundEnvironment.getElementsAnnotatedWith(Bind.class)获取
     * 被注解元素的信息的集合。
     * @return
     */

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {


        /**
         * 哪些地方用了Bind注解,就返回这个Element
         * Element一般有几下几种类型:
         - VariableElement //一般代表成员变量
         - ExecutableElement //一般代表类中的方法
         - TypeElement //一般代表代表类
         - PackageElement //一般代表Package
         */
        for (Element element : roundEnvironment.getElementsAnnotatedWith(Bind.class)) {
            System.out.println("------------------------------");
            System.out.println("TypeMirror---"+element.asType());
            System.out.println("TypeMirror.getKind()---"+element.asType().getKind());
            System.out.println("ElementKind---"+element.getKind());
            System.out.println("element---"+element);
            System.out.println("element.getSimpleName()---"+element.getSimpleName());
            // 判断元素的类型为Field
            if (element.getKind() == ElementKind.FIELD){
                VariableElement vElement = (VariableElement) element;
                //System.out.println("VariableElement---"+vElement);
                System.out.println(vElement.getAnnotation(Bind.class).value());
            }
            System.out.println("------------------------------");
        }
        return false;
    }
}


我们先直接Rebuild Project一把,然后对应的打印结果:

下面我们重点来说下Element,然后再回头来看打印结果。

Element元素

  • Element里面能拿到ElementKind,Name等一系列属性:

这里写图片描述

  • ElementKind类:

这里写图片描述

  • Element子类:

这里写图片描述

其实就是跟meta元素标签类似的对一个类的一种封装形式。再回头看打印结果,应该能弄清楚是怎么一回事了。示例中,我们把Bind注解用在成员上,所以elementKind是Field,Element一般有几下几种类型:

VariableElement //一般代表成员变量
ExecutableElement //一般代表类中的方法
TypeElement //一般代表代表类
PackageElement //一般代表Packageelement跟element.getSimpleName就是成员的名称

TypeMirror是这个成员的Class全称。嗯~~基本就是这样,如果你想弄清楚更多,直接一个个编译打印一次就知道了。

Messager

Messager相当于一个信息跟踪系统,跟log日志类似,我们主要用来追踪错误。

if (annotatedElement.getKind() != ElementKind.FIELD)//被注解的元素不是成员
        {
            error(annotatedElement, "%s must be declared on field.", clazz.getSimpleName());
            return false;
        }
        if (ClassValidator.isPrivate(annotatedElement))//成员是私有的
        {
            error(annotatedElement, "%s() must can not be private.", annotatedElement.getSimpleName());
            return false;
        }

private void error(Element element, String message, Object... args) {
        if (args.length > 0) {
            message = String.format(message, args);
        }
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);
}
//*****************************************************************************  
//Diagnostic.Kind有几种类型:  
public static enum Kind {
        ERROR,
        WARNING,
        MANDATORY_WARNING,
        NOTE,
        OTHER;
        private Kind() {
        }
}

比如我们将Bind注解定义在class上,Rebuild的时候就会报如下错误:

这里写图片描述

JavaPoet和Filer

通过注解处理器来生成 .java 源文件基本上都会使用javapoet 这个库,JavaPoet一个是用于产生 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件,(当然你也可以不用这个JavaPoet库,自己手写,参考示例中ViewInjectProcessor操作)下面来看下具体使用方法。
首先我们需要引入JavaPoet库到viewinject-compiler中:compile ‘com.squareup:javapoet:1.9.0’

MyProcess类:

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

    private Filer filer;

    /**
     * processingEnvironment提供Elements,Types和Filer等生成类(.java文件)的工具
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

    /**
     * 这个方法返回一个字符串的集合,你写的注解类都会装在这个集合里面。注解处理器会根据
     * 处理集合里面所有的注解
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        //annotations.add(Bind.class.getCanonicalName());
        annotations.add(MyAnnotation.class.getCanonicalName());
        return annotations;
    }

    /**
     * 指定使用的Java版本,一般我们返回SourceVersion.latestSupported()
     *
     * @return java版本
     * public enum SourceVersion {
     * RELEASE_0,
     * RELEASE_1,
     * RELEASE_2,
     * RELEASE_3,
     * RELEASE_4,
     * RELEASE_5,
     * RELEASE_6,
     * RELEASE_7,
     * RELEASE_8;
     * }
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 看到set和roundEnvironment两个参数了吧
     *
     *
     * @param set
     * @param roundEnvironment
     *
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        for (TypeElement element : set) {
            if (element.getQualifiedName().toString().equals(MyAnnotation.class.getCanonicalName())) {
                // 创建main方法
                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();
                // 创建HelloWorld类
                TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addMethod(main)
                        .build();

                try {
                    // 生成 com.example.HelloWorld.java
                    JavaFile javaFile = JavaFile.builder("com.example.viewinject_sample", helloWorld)
                            .addFileComment(" This codes are generated automatically. Do not modify!")
                            .build();
                    // 生成文件
                    javaFile.writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return true;
        }
        return false;
    }
}

Rebuild Project一把,生成了HelloWorld类:

这里写图片描述

@AutoService(Processor.class)

其实我们的注解器只有注册了才能在编译的时候产生作用。相当于一个登记作用。如下:
这里写图片描述

我们如果加了这句,才会有这个Processor文件,并且文件里面是我们自己的注解器。其实我们可以不需要这个auto-service库,自己手工操作登记也可以,具体流程:
1、在 viewinject-compiler 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

这样注解处理器已经可以正常工作了,跟引入auto-service库效果一样~

android-apt和annotationProcessor

我们都会在project级别的build.gradle配置这么一句:
这里写图片描述
module中配置这些:
这里写图片描述

这里写图片描述

大体来讲它有两个作用:

1.允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library
2.设置源路径,使注解处理器生成的代码能被Android Studio正确的引用

第一个作用比较好理解:但注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,而主项目添加了这个库会引入很多不必要的文件。第二个作用暂时没发现不用的话有什么弊端。

当然了,Android Studio 2.2 Gradle 插件提供了annotationProcessor 的功能来代替 android-apt . 而且android-apt 后续将不会继续维护. 所以将android-apt切换annotationProcessor吧。在module中只需配置一句:annotationProcessor project(‘:viewinject-compiler’)就可以了。

以上差不多就是整个apt的套路了。

参考以下文章:
https://race604.com/annotation-processing/
http://blog.csdn.net/github_35180164/article/details/52121038
示例源码地址:https://github.com/hymanAndroid/ioc-apt-sample

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值