Android 注解系列之APT工具(三)

该文章中涉及的代码,我已经提交到GitHub上了,你们按需下载---->源码html

前言

在上篇文章Android 注解系列之Annotation(二)中,简要的介绍了注解的基本使用与定义。同时也提出了如下几个问题,当咱们声明了一个注解后,是否是须要手动找到全部的Class对象或Field、Method?怎么经过注解生成新的类的定义呢?当面对这些问题的时候,我相信你们的第一反应确定会想,"有不有相应的三方库呢?Java是否提供了相应库或者方法来解决呢?",固然Java确定给咱们提供了啦,就是咱们既陌生又熟悉的APT工具啦。java

为何这里我会说既陌生又熟悉呢?我相信对于大多数安卓程序,咱们都或多或少使用了一些主流库,如Dagger二、ButterKnife、EventBus等,这些库都使用了APT技术。既然大佬们都在使用,那咱们怎么不去了解呢?好了,书归正传,下面咱们就来看看怎么经过APT来处理以前咱们提到的问题。android

APT技术简介

在具体了解APT技术以前,先简单的对其进行介绍。APT(Annotation Processing Tool)是javac中提供的一种编译时扫描和处理注解的工具,它会对源代码文件进行检查,并找出其中的注解,而后根据用户自定义的注解处理方法进行额外的处理。APT工具不只能解析注解,还能根据注解生成其余的源文件,最终将生成的新的源文件与原来的源文件共同编译(注意:APT并不能对源文件进行修改操做,只能生成新的文件,例如在已有的类中添加方法)。具体流程图以下图所示:git

apt使用流程图.png

APT技术使用规则

APT技术的使用,须要咱们遵照必定的规则。你们先看一下整个APT项目项目构建的一个规则图,具体以下所示: github

apt_rule.png

APT使用依赖

从图中咱们能够整个APT项目的构建须要三个部分:数据库

  • 注解处理器库(包含咱们的注解处理器)
  • 注解声明库(用于存储声明的注解)
  • 实际使用APT的Android/Java项目

且三个部分的依赖关系为注解处理工具依赖注解声明库Android/Java项目同时依赖注解处理工具库与注解声明库api

为何把注解处理器独立抽成一个库呢?

对于Android项目默认是不包含 APT相关类的。因此要使用APT技术,那么就必须建立一个Java Library。对于Java项目,独立抽成一个库,更容易维护与扩展。数组

为何把注解声明也单独抽成一个库,而不放到注解处理工具库中呢?

举个例子,若是注解声明与注解处理器为同一个库,若是有开发者但愿把咱们的注解处理器用于他的项目中,那么就必须包含注解声明与整个注解处理器的代码,咱们能很是肯定是,他并不但愿已经编译好的项目中包含处理器相关的代码。他仅仅但愿使用咱们的注解。因此将注解处理器与注解分开单独抽成一个库时很是有意义的。接下来的文章中会具体会描述有哪些方法能够将咱们的注解处理器不打包在咱们的实际项目中。bash

注解处理器的声明

在了解了ATP的使用规则后,如今咱们再来看看怎么声明一个注解处理器,每个注解处理器都须要承AbstractProcessor类,具体代码以下所示:markdown

class MineProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {}
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() { }

    @Override
    public Set<String> getSupportedAnnotationTypes() { }
}
复制代码
  • init(ProcessingEnvironment processingEnv):每一个注解处理器被初始化的时候都会被调用,该方法会被传入ProcessingEnvironment 参数。ProcessingEnvironment 能提供不少有用的工具类,Elements、Types和Filer。后面咱们将会看到详细的内容。
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):注解处理器实际处理方法,通常要求子类实现该抽象方法,你能够在在这里写你的扫描与处理注解的代码,以及生成Java文件。其中参数RoundEnvironment ,可让你查询出包含特定注解的被注解元素,后面咱们会看到详细的内容。
  • getSupportedAnnotationTypes(): 返回当前注解处理器处理注解的类型,返回值为一个字符串的集合。其中字符串为处理器须要处理的注解的合法全称
  • getSupportedSourceVersion():用来指定你使用的Java版本,一般这里返回SourceVersion.latestSupported()。若是你有足够的理由指定某个Java版本的话,你能够返回SourceVersion.RELAEASE_XX。可是仍是推荐使用前者。

在Java1.6版本中提供了SupportedAnnotationTypesSupportedSourceVersion两个注解来替代getSupportedSourceVersiongetSupportedAnnotationTypes两个方法,也就是这样:

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes({"合法注解的名称"})
class MineProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
    
}

复制代码

这里须要注意的是以上提到的两个注解是JAVA 1.6新增的,因此出于兼容性的考虑,建议仍是直接重写getSupportedSourceVersion()getSupportedAnnotationTypes()方法。

注册注解处理器

到了如今咱们基本了解了处理器声明,如今咱们可能会有个疑问,怎么样将注解处理器注册到Java编译器中去呢?你必须提供一个.jar文件,就像其余.jar文件同样,你须要打包你的注解处理器到此文件中,而且在你的jar中,你须要打包一个特定的文件javax.annotation.processing.ProcessorMETA-INF/services路径下。就像下面这样:

META-INF/services 至关于一个信息包,目录中的文件和目录得到Java平台的承认与解释用来配置应用程序、扩展程序、类加载器和服务文件,在jar打包时自动生成

放入特定文件夹.png

其中javax.annotation.processing.Processor文件中的内容为每一个注解处理器的合法的全名列表,每个元素换行分割,也就是相似下面这样:

com.jennifer.andy.processor.MineProcessor1
com.jennifer.andy.processor.MineProcessor2
com.jennifer.andy.processor.MineProcessor3
复制代码

最后咱们只要将你生成的.jar放到你的buildPath中,那么Java编译器会自动的检查和读取javax.annotation.processing.Processor中的内容,并注册该注解处理器。

固然对于如今咱们的编译器,如IDEA、AndroidStudio等中,咱们只建立相应文件与文件夹就好了,并不一样用放在buildPath中去。固然缘由是这些编译器都帮咱们处理了啦。若是你仍是嫌麻烦,那咱们可使用Google为咱们提供的AutoService 注解处理器,用于生成META-INF/services/javax.annotation.processing.Processor文件的。也就是咱们能够像下面这样使用:

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes({"合法注解的名称"})
@AutoService(Processor.class)
class MineProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}
复制代码

咱们只须要在类上声明@AutoService(Processor.class),那么就不用考虑其余的东西啦。是否是很方便呢?(固然使用AutoService在Gralde中你须要添加依赖compile 'com.google.auto.service:auto-service:1.0-rc2')。

注解处理器的扫描

在注解处理过程当中,咱们须要扫描全部的Java源文件,源代码的每个部分都是一个特定类型的Element,也就是说Element表明源文件中的元素,例如包、类、字段、方法等。总体的关系以下图所示:

element继承关系.png

  • Parameterizable:表示混合类型的元素(不只只有一种类型的Element)
  • TypeParameterElement:带有泛型参数的类、接口、方法或者构造器。
  • VariableElement:表示字段、常量、方法或构造函数。参数、局部变量、资源变量或异常参数。
  • QualifiedNameable:具备限定名称的元素
  • ExecutableElement:表示类或接口的方法、构造函数或初始化器(静态或实例),包括注释类型元素。
  • TypeElement :表示类和接口
  • PackageElement:表示包

那接下来咱们经过下面的例子来具体的分析:

package com.jennifer.andy.aptdemo.domain;//PackageElement
class Person {//TypeElement 
    private String where;//VariableElement
    
    public void doSomething() { }//ExecutableElement
    
    public void run() {//ExecutableElement
        int runTime;//VariableElement
    }
}
复制代码

经过上述例子咱们能够看出,APT对整个源文件的扫描。有点相似于咱们解析XML文件(这种结构化文本同样)。

既然在扫描的时候,源文件是一种结构化的数据,那么咱们能不能获取一个元素的父元素和子元素呢?。固然是能够的啦,举例来讲,假如咱们有个public class Person的TypeElement元素,那么咱们能够遍历它的全部的孩子元素。

TypeElement person= ... ;  
for (Element e : person.getEnclosedElements()){ // 遍历它的孩子 
    Element parent = e.getEnclosingElement();  // 拿到孩子元素的最近的父元素
}
复制代码

其中getEnclosedElements()getEnclosingElement()Element中接口的声明,想了解更多的内容,你们能够查看一下源码。

元素种类判断

如今咱们已经了解了Element元素的分类,可是咱们发现Element有时会表明多种元素。例如TypeElement表明类或接口,那有什么方法具体区别呢?咱们继续看下面的例子:

public class SpiltElementProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
	    //这里经过获取全部包含Who注解的元素set集合
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.CLASS) {//若是元素是类

            } else if (element.getKind() == ElementKind.INTERFACE) {//若是当前元素是接口

            }
        }
        return false;
    }
	...省略部分代码
}
复制代码

在上述例子中,咱们经过roundEnvironment.getElementsAnnotatedWith(Who.class)获取源文件中全部包含@Who注解的元素,经过调用element.getKind()具体判断当前元素种类,其中具体元素类型为ElementKind枚举类型ElementKind枚举声明以下表所示:

枚举类型 种类
PACKAGE
ENUM 枚举
CLASS
ANNOTATION_TYPE 注解
INTERFACE 接口
ENUM_CONSTANT 枚举常量
FIELD 字段
PARAMETER 参数
LOCAL_VARIABLE 本地变量
EXCEPTION_PARAMETER 异常参数
METHOD 方法
CONSTRUCTOR 构造函数
OTHER 其余
省略... 省略...

元素类型判断

那接下来你们又会有一个问题了,既然咱们在扫描的是获取的元素且这些元素表明着源文件中的结构化数据。那么假如咱们想得到元素更多的信息怎么办呢?例如对于某个类,如今咱们已经知道了其为ElementKind.CLASS种类,可是我想获取其父类的信息,须要经过什么方式呢?对于某个方法,咱们也一样知道了其为ElementKind.METHOD种类,那么我想获取该方法的返回值类型、参数类型、参数名称,须要经过什么方式呢?

固然Java已经为咱们提供了相应的方法啦。使用mirror API就能解决这些问题啦,它能使咱们在未经编译的源代码中查看方法、域以及类型信息。在实际使用中经过TypeMirror来获取元素类型。看下面的例子:

public class TypeKindSpiltProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.METHOD) {//若是当前元素是接口
                ExecutableElement methodElement = (ExecutableElement) element;
                TypeMirror returnType = methodElement.getReturnType();//获取TypeMirror
                TypeKind kind = returnType.getKind();//获取元素类型
                System.out.println("print return type----->" + kind.toString());
            }
        }
        return false;
    }

}
复制代码

观察上述代码咱们能够发现,当咱们使用注解处理器时,咱们会先找到相应的Element,若是你想得到该Element的更多的信息,那么能够配合TypeMirror使用TypeKind来判断当前元素的类型。固然对于不一样种类的Element,其获取的TypeMirror方法可能会不一样。TypeKind枚举声明以下表所示:

枚举类型 类型
BOOLEAN boolean 类型
BYTE byte 类型
SHORT short 类型
INT int 类型
LONG long 类型
CHAR char 类型
FLOAT float 类型
DOUBLE double 类型
VOID void类型,主要用于方法的返回值
NONE 无类型
NULL 空类型
ARRAY 数组类型
省略... 省略...

元素可见性修饰符

在注解处理器中,咱们不只能得到元素的种类和信息,咱们还能获取该元素的可见性修饰符(例如public、private等)。咱们能够直接调用Element.getModifiers(),具体代码以下所示:

public class GetModifiersProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.CLASS) {//若是元素是类
                Set<Modifier> modifiers = element.getModifiers();//获取可见性修饰符
                if (!modifiers.contains(Modifier.PUBLIC)) {//若是当前类不是public
                    throw new ProcessingException(classElement, "The class %s is not public.",
                            classElement.getQualifiedName().toString());
                }
            }
        return false;
    }
}
复制代码

在上述代码中Modifer为枚举类型,具体枚举以下所示:

public enum Modifier {

    /** The modifier {@code public} */          PUBLIC,
    /** The modifier {@code protected} */       PROTECTED,
    /** The modifier {@code private} */         PRIVATE,
    /** The modifier {@code abstract} */        ABSTRACT,
    /**
     * The modifier {@code default}
     * @since 1.8
     */
     DEFAULT,
    /** The modifier {@code static} */          STATIC,
    /** The modifier {@code final} */           FINAL,
    /** The modifier {@code transient} */       TRANSIENT,
    /** The modifier {@code volatile} */        VOLATILE,
    /** The modifier {@code synchronized} */    SYNCHRONIZED,
    /** The modifier {@code native} */          NATIVE,
    /** The modifier {@code strictfp} */        STRICTFP;
}

复制代码

错误处理

在注解处理器的自定义中,咱们不只能调用相关方法获取源文件中的元素信息,还能经过处理器提供的Messager来报告错误、警告以及提示信息。能够直接使用processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);须要注意的是它并非处理器开发中的日志工具,而是用来写一些信息给使用此注解库的第三方开发者的。也就是说若是咱们像传统的Java应用程序抛出一个异常的话,那么运行注解处理器的JVM就会崩溃,而且关于JVM中的错误信息对于第三方开发者并非很友好,因此推荐而且强烈建议使用Messager。就像下面这样,当咱们判断某个类不是public修饰的时候,咱们经过Messager来报告错误。

注解处理器是运行它本身的虚拟机JVM中。是的,你没有看错,javac启动一个完整Java虚拟机来运行注解处理器。

public class GetModifiersProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Who.class);
        for (Element element : elements) {
            if (element.getKind() == ElementKind.CLASS) {//若是元素是类
                Set<Modifier> modifiers = element.getModifiers();//获取可见性修饰符
                if (!modifiers.contains(Modifier.PUBLIC)) {//若是当前类不是public
	                roundEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "the class is not public");
                }
            }
        return false;
    }
}
复制代码

同时,在官方文档中,描述了消息的不一样级别,关于更多的消息级别,你们能够经过从Diagnostic.Kind枚举中查看。

错误信息显示界面

若是你须要使用处理器提供的 Messager 来打印日志,那么你须要在以下界面中查看输出的信息:

roundEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "the class is not public");
复制代码

使用如上代码,查看日志界面以下所示:

messager错误展现界面.png

在每次编译代码的时候,若是你使用了 Messager 来打印日志,那么就会显示。

文件生成

到了如今咱们已经基本了解整个APT的基础知识。如今来说讲APT技术如何生成新的类的定义(也就是建立新的源文件)。对于建立新的文件,咱们并不用像基本文件操做同样,经过调用IO流来进行读写操做。而是经过JavaPoet来构造源文件。(固然当你使用JavaPoet时,在gradle中你须要添加依赖compile 'com.google.auto.service:auto-service:1.0-rc2'),JavaPoet的使用也很是简单,就像下面这样:

当进行注释处理或与元数据文件(例如,数据库模式、协议格式)交互时,JavaPoet对于源文件的生成可能很是有用。经过生成代码,消除了编写样板的必要性,同时也保持了元数据的单一来源。

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.jennifer.andy.apt.annotation.Who")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class CreateFileByJavaPoetProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        createFileByJavaPoet(set, roundEnvironment);
        return false;
    }
    
    /**
     * 经过JavaPoet生成新的源文件
     */
    private void createFileByJavaPoet(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //建立main方法
        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//设置可见性修饰符public static
                .returns(void.class)//设置返回值为void
                .addParameter(String[].class, "args")//添加参数类型为String数组,且参数名称为args
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//添加语句
                .build();
        //建立类
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)//将main方法添加到HelloWord类中
                .build();

        //建立文件,第一个参数是包名,第二个参数是相关类
        JavaFile javaFile = JavaFile.builder("com.jennifer.andy.aptdemo.domain", helloWorld)
                .build();

        try {
            //建立文件
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            log(e.getMessage());
        }

    }

    /**
     * 调用打印语句而已
     */
    private void log(String msg) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
    }

}
复制代码

当咱们build上述代码后,咱们能够在咱们的build目录下获得下列文件:

生成文件结果.png

关于JavaPoet的更多的详细使用,你们能够参考官方文档-------->JavaPoet

分离处理器和项目

在上文中描述的APT使用规则中,咱们是将注解声明库注解处理器库分红了两个库,具体缘由我也作了详细的解释,如今咱们来思考以下问题。就算咱们把两个库都抽成了两个独立的库,可是若是有开发者想把咱们自定义的注解处理器用于他的项目中,那么他整个项目的编译就必须也要把注解处理器与注解声明库包括进来。对于开发者来讲,他们并不但愿已经编译好的项目中有包含注解处理器的相关代码。因此将注解声明库与注解处理器库不打包进入项目是很是有必要的!!换句话说,注解处理器只在编译处理期间须要用到,编译处理完后就没有实际做用了,而主项目添加了这个库会引入不少没必要要的文件。

由于做者我自己是Android开发人员,因此如下都是针对Android项目展开讨论。

使用android-apt

anroid-apt是Hugo Visser开发的一个Gradle插件,该插件的主要做用有以下两点:

  • 容许只将编译时注释处理器配置为依赖项,而不在最终APK或库中包括工件
  • 设置源路径,以便Android Studio能正确地找到注释处理器生成的代码

可是 Google爸爸看到别人这个功能功能不错,因此为本身的Android Gradle 插件也添加了名为annotationProcessor 的功能来彻底代替 android-apt,既然官方支持了。那咱们就去看看annotationProcessor的使用吧。

annotationProcessor使用

其实annotationProcessor的使用也很是简单,分为两种类型,具体使用以下代码所示:

annotationProcessor project(':apt_compiler')//若是是本地库
 annotationProcessor 'com.jakewharton:butterknife-compiler:9.0.0-rc1'//若是是远程库
复制代码

总结

整个APT的流程下来,本身也查阅了很是多的资料,也解决了许多问题。虽然写博客也花了很是多的时间。可是本身也发现了不少有趣的问题。我发现查阅的相关资料都会有一个通病。也就是没有真正搞懂android apt与annotationProcessor的具体做用。因此这里这里也要告诫你们,对于网上的资料,本身必定要带着怀疑与疑问的态度去浏览

同时我的以为Gradle这一块的知识点也很是重要。由于关于怎么不把库打包到实际项目中也是构建工具的特性与功能。但愿你们有时间,必定要学习下相关Gradle知识。做者最近也在学习呢。和我一块儿加油吧~

该文章中涉及的代码,我已经提交到GitHub上了,你们按需下载---->源码

最后

该文章参考如下博客与图书,站在巨人的肩膀上。能够看得更远。

ANNOTATION PROCESSING 101

自定义注解之编译时注解(RetentionPolicy.CLASS)

你必须知道的APT、annotationProcessor、android-apt、Provided、自定义注解

相关文章
相关标签/搜索