编译期代码动态生成之APT、JavaPoet的使用

在Android中,我们经常使用一些开源项目,如:butterknife、dagger、ARouter等,这些开源项目会根据特定的注解生成特定的java代码使我们的项目的开发更加高效。说到注解,不得不提一下这种方式与常规运行时注解使用的区别:

  • 运行时注解(Runtime)
    在代码运行时通过反射机制进行处理的注解,这种注解因为反射的缘故往往会对程序的性能有一定的影响。
  • 编译时注解(Compile time)
    在编译期间就进行处理的注解。

本篇文章介绍的就是编译时注解,并介绍处理这种编译期注解的手段APTJavaPoet

注解简介

在某些代码元素上(如:类型、函数、字段等)添加注解,在编译时编译期会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理。如:根据注解生成java类,这也是ARouter等开源库的基本原理。

注解分类

注解根据功能、运行时机不同一共分为三类:标记类注解、运行时注解、编译时注解我们根据@Retention注解传入的值不同来区分三种注解

标记类注解

标记一些信息,如经常用到的@Override@SuppressWarnings等,这些注解都仅仅用于标记,主要是给IDE看的,可以用作一些检验。

该类注解对应RetentionPolicy.SOURCE

下面是@Override的源码

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

运行时注解

运行时处理的注解,这也是我们最常用的一种注解使用方式,在运行时拿到类的对象,然后通过反射做相关的处理。

运行时注解对应RetentionPolicy.RUNTIME

例:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited//该注解可以被继承
public @interface SlothContentView {
    @LayoutRes
    int toolBarViewId() default View.NO_ID;
    @LayoutRes
    int contentViewId() default View.NO_ID;
}
//通过反射获取注解及注解值的过程,假设该类是存在该注解的(省略空判断和获取的具体值的处理)
Class<?> clazz = object.getClass();
SlothContentView slothContentView = clazz.getAnnotation(SlothContentView.class);
int contentViewId = slothContentView.contentViewId();
int toolbarViewId = slothContentView.toolBarViewId();

上面的这个注解用于给Fragment/Activity插入布局,我们可以通过该注解传入主体和标题栏的布局id,然后通过一系列操作对其进行赋值,这样可以免去常规的赋值方法,并节省很多代码。

编译时注解

在编译的时候根据注解标识,进行一些处理,本篇讲的就是这种注解,我们的目的是在编译期间根据注解内容,动态生成一些类或者生成一些文件等,因为是在编译期间生成的文件,所以使用起来和手写的没什么区别,不存在反射等操作,所以性能会比运行时注解高一些,不过二者针对不同场景使用,没有可比性。

运行时注解对应RetentionPolicy.CLASS

例:

@Retention(RetentionPolicy.CLASS)  
@Target({ ElementType.FIELD, ElementType.TYPE })  
public @interface InjectView{  
    int value();  
}

上面是一个编译时注解的声明,可以根据自己业务的不同进行不同的处理,由于本文讲的就是编译时注解的两种处理方式,所以在此不再进行阐述。

注解作用对象

我们在使用注解的时候,有些注解可以在类上使用,也可以在方法上使用,但有些注解只能在变量上使用,我们可以在定义注解的时候为其指定作用目标,可以为一个注解指定多个目标。
我们使用@Target注解作为注解的作用对象,@Target接受值为ElementType[]ElementType是一个枚举类。

ElementType中常量的含义

名称 作用对象
TYPE 接口、类、枚举、注解
FIELD 方法、枚举的常量
METHOD 方法
PARAMETER 方法参数
CONSTRUCTOR 构造函数
LOCAL_VARIABLE 局部变量
ANNOTATION_TYPE 注解
PACKAGE

APT(Annotation Processor Tool)

APT(Annotation Processor Tool),即注解处理器。

什么是APT?

APT是一种处理注解的工具,确切的说它是javac的一个工具,用在编译时扫描和处理注解,一个注解的注解处理器,以java代码(或者编译过的字节码)作为输入,生成.java文件作为输出,核心是交给自己定义的处理器去处理。

APT的使用

我们需要在AndroidStudio中使用APT,我们新建一个AndroidStudio项目后,先进行APT的配置。

配置

第一步:在项目build.gradle中配置

buildscript{
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

第二步:在项目主module中配置

apply plugin: 'com.neenbedankt.android-apt'

上面算是对apt的初步配置。但是在使用过程中可能会遇到其他问题,到时候再进行讲解。

使用

定义继承自虚处理器AbsrtactProcessor类,并实现其中比较关键的几个方法。

找不到AbstractProcessor类?

新建一个java module,然后在这个module中建立处理器,然后集成AbstractProcessor。当要使用的时候,让要使用的module关联这个java module。

public class HelloProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}

下面我们介绍一下AbstractProcessor中这几个主要方法的含义。

  • init(ProcessingEnvironment env)

每个注解处理器类必须要有一个空的构造方法。这个方法会被注解处理工具调用,并输入ProcessingEnvironment参数。ProcessingEnvironment提供很多工具类。该类的声明如下:

public interface ProcessingEnvironment {
    Map<String, String> getOptions();
    Messager getMessager();
    Filer getFiler();
    Elements getElementUtils();
    Types getTypeUtils();
    SourceVersion getSourceVersion();
    Locale getLocale();
}
  • process(Set<? extends TypeElement> annotations, RoundEnvironment env)

这个方法相当于每个处理器的主函数main()。可以在此处处理自己的逻辑,如:扫描、评估和处理注解的代码,以及生成代码、参数RoundEnvironment可以查询出包含特定注解的被注解元素。返回值是一个boolean值,表明注解是否以及被处理器处理完成,通常在出现异常的时候返回false,执行完毕则返回true

  • getSupportedAnnotationTypes()

该方法要实现,用来表示这个注解处理器是注册给哪个注解的。返回值是一个String类型的Set集合,包含处理器想要处理的注解类型的合法全称。

  • getSupportedSourceVersion()

用来制定Java版本,返回值SourceVersion是一个枚举类型,通常返回SourceVersion.latestSupported()

实战

定义一个编译时注解,并通过APT工具生成一个完整的java代码并成功执行。

定义注解

定义注解@HelloAPT

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface HelloAPT {
    String value() default "hello apt";
}

当项目中有该注解存在时,生成一个HelloApt.java文件,并在其中的main方法中打印出该注解的value的值。

注册注解处理器

由于处理器是javac中的工具,因此我们必须将我们自己的处理器注册到javac中,在以前我们需要提供一个.jar文件,打包注解处理器到这个文件中,并在jar中打包一个特定的文件javax.annotation.processing.ProcessorMETA-INF/services路径下,然后把HelloProcessor.jar放到buildpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册HelloProcessor为注解解释器。

从上面可以看出,这种处理是十分复杂的,好在google提供了可以让我们方便开发的工具,我们直接使用@AutoService注解来达到以上目的。谷歌auto工具入口

要使用该注解,可以通过gradle导入:

compile 'com.google.auto.service:auto-service:1.0-rc2'

此时我们就可以使用该注解了,给该类打的完整注解如下:

@AutoService(Processor.class)

google-auto原文的解释如下:

AutoService will generate the file META-INF/services/javax.annotation.processing.Processor in the output classes folder. The file will contain:foo.bar.MyProcessor,In the case of javax.annotation.processing.Processor, if this metadata file is included in a jar, and that jar is on javac’s classpath, then javac will automatically load it, and include it in its normal annotation processing environment. Other users of java.util.ServiceLoader may use the infrastructure to different ends, but this metadata will provide auto-loading appropriately.

APT中的Elements和TypeMirrors

init方法中,我们可以获取到ProcessingEnvironment参数,而通过该参数,我们可以获取到一些工具类。

  • Elements:一个用于处理Element的工具类。
  • Types:一个用于处理TypeMirror的工具类。
  • Filer:可以用于创建文件(通常与javapoet结合使用)。

在注解处理过程中,我们扫描的所有java源文件,每个部分都是一个特定的Element
对于编译器来说,代码中的元素结构组成部分是一致的,如:包、类、函数、字段、变量等,JDK为这些元素定义了一个积累,即Element

Element一共有五个直接子类,分别代表一种特定元素。

名称 含义
PackageElement 包程序元素,可以获取到包名等
TypeParameterElement 一般类、接口、方法或者构造方法元素的泛型参数
TypeElement 一个类或接口程序元素
VariableElement 一个字段、enum常亮、方法或构造方法参数、局部变量或异常参数
ExecutableElement 某个类或接口的方法、构造方法或初始化程序(静态实例),包括注解类型元素

下面的代码解释一个类中的各个元素的对应值

package com.jkb.apt;    //PackageElement
public class MyClass<T> {  //TypeElement
    private int a;      //VariableElement
    private String b;    //VariableElement
    private MyClass instance;   //VariableElement
    private T t;    //TypeParameterElement
    public MyClass(T t) {  //ExecutableElement
        this.t = t;     //泛型参数t为TypeParameterElement
    }
    public void setA(int a) {   //ExecutableElement
        this.a = a;
    }
}
重写getSupportedSourceVersion()方法和getSupportedAnnotationTypes()方法

前面提到过,我们需要重写四个方法,我们先复写最简单的两个方法,两个方法的含义在前面也介绍过。代码如下:

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> result = new HashSet<>();
    result.add(HelloAPT.class.getName());
    return result;
}

@Override
public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
}
重写init()方法

init()方法传递进来的参数ProcessingEnvironment提供了一系列的工具类,我们声明一些可能会用到的工具类并在此方法中进行赋值。

private Filer filer;
private Elements elements;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    filer = processingEnvironment.getFiler();
    elements = processingEnvironment.getElementUtils();
}
重写process()方法

process()方法是我们进行处理的主要方法,传递进来有两个参数:Set<? extends TypeElement>RoundEnvironment,下面我们对两个参数进行解释:

RoundEnvironment类中方法解释

返回值 方法名 解释
boolean errorRaised() 如果在以前的处理round中发生错误,则返回true;否则返回false
Set<? extends Element> getRootElements() 返回以前round生成的注解处理根元素
Set<? extends Element> getElementsAnnotatedWith(TypeElement var1) 返回给定的特定元素的Element元素
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1) 获取给定注解的Element元素

Set<? extends TypeElement>参数
该参数是一个类/接口程序元素的Set集合。

在本文处理中,我们只需要获取带有HelloAPT注解的元素即可,而从上面HelloAPT定义可以看出,该注解只可以使用在类上面(@Target({ElementType.TYPE}))。

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    //获取被该注解声明的元素
    Set<? extends Element> annotationSet = roundEnvironment.getElementsAnnotatedWith(HelloAPT.class);
    for (Element element : annotationSet) {
        //判断元素类型
        ElementKind kind = element.getKind();
        if (kind != ElementKind.CLASS) {
            System.out.println("HelloAPT Annotation can only used on class");
            return false;
        }
    }
    return true;
}

在上面的代码中,我们获取了HellpAPT注解作用的所有元素,并对不是class类型的进行了处理(其实注解在定义的时候已经指定了作用对象,此处可以不用处理)。

接下来我们需要对获取到的元素进行操作并生成java代码。

我们对上面的代码做一些扩展,在遍历的时候获取到所有的TypeElement类型的元素。

List<TypeElement> typeElements;//循环遍历到的所有元素
genrateCode(typeElements);//生成代码
private boolean generateCodes(List<TypeElement> typeElements) {
    //判断是否有含有元素
    if (typeElements == null || typeElements.isEmpty()) {
        logger.w("@HelloProcessor is not found!!!");
        return false;
    }
    //遍历元素并生成响应的类文件
    for (TypeElement element : typeElements) {
        //获取注解
        HelloAPT annotation = element.getAnnotation(HelloAPT.class);
        if (annotation == null) continue;
        //获取注解中的值,类名等用于生成文件的时候使用
        String annotationValue = annotation.value();
        String simpleName = element.getSimpleName().toString();
        logger.i("----->" + simpleName + "<-----" + annotationValue);
        //创建java文件
        try {
            simpleName += "$" + "print";
            //通过init方法中得到的filer对象进行操作。
            FileObject fileObject = filer.createResource(StandardLocation.SOURCE_OUTPUT, Consts
                    .PACKAGE_OF_GENERATE_FILE, simpleName + ".java");
            //一步步写入内容
            Writer writer = fileObject.openWriter();
            writer.append("package ").append(Consts.PACKAGE_OF_GENERATE_FILE).append(";").append("\n");
            writer.append("/*This file is auto created by APT,please don't edit it!!!*/").append("\n");
            writer.append("public ").append("class ").append(simpleName).append("{").append("\n");
            writer.append("public static void main(String[]args)").append("{").append("\n");
            writer.append("System.out.println(").append("\"").append(annotationValue).append("\"").append(");");
            writer.append("\n");
            writer.append("}");
            writer.append("\n");
            writer.append("}");
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }
    return true;
}

上面的generateCodes()方法生成简单的类,生成类名$print.java文件并在里面生成main方法然后打印出HelloAPT中的value的值。

以下是代码运行截图(我在MainActivity中使用了注解):
控制台输入结果
生成代码目录
生成类

以上是我们build之后输出信息及生成文件截图,但是我们回过头看看生成代码的过程,看一眼还是别看了,辣眼睛!!!这还只是一个简单的类,要是类的结构复杂了怎么办???我们还要不要用APT呢???答案是肯定的,我们下面介绍一种专门为了生成文件的神器JavaPoet

JavaPoet

JavaPoet是为了方便生成.java源文件所提供的库。

原文介绍如下:

JavaPoet is a Java API for generating .java source files.
Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.

配置

build.gradle中引入即可使用。

compile 'com.squareup:javapoet:1.9.0'

项目传送门JavaPoet

实战

在该项目的主页上有比较详细的说明,就不在本篇进行阐述,如有需要请移步查看。

我们重写另起一个用于写文件的方法。

private boolean generateCodesByPoet(List<TypeElement> typeElements) {
    if (typeElements == null || typeElements.isEmpty()) {
        logger.w("@HelloProcessor is not found!!!");
        return false;
    }
    for (TypeElement element : typeElements) {
        HelloAPT annotation = element.getAnnotation(HelloAPT.class);
        if (annotation == null) continue;
        String annotationValue = annotation.value();
        String className = element.getSimpleName().toString() + "$Poet";
        logger.i("----->" + className + "<-----" + annotationValue);        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class, "args")
                .addStatement("$T.out.println($S)", System.class, annotationValue)
                .build();
        TypeSpec classSpec = TypeSpec.classBuilder(className)
                .addJavadoc(Consts.WARNING_TIPS)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(main)
                .build();
        try {
            JavaFile.builder(Consts.PACKAGE_OF_GENERATE_FILE, classSpec)
                    .build().writeTo(filer);
        } catch (IOException e) {
            return false;
        }
    }
    return true;
}

可以看出,JavaPoet通过builder模式进行类文件的构建,不管是从代码外观上还是可读性上都得到了提高。

生成的类截图:
JavaPoet生成代码
上面生成的代码都没有进行过代码格式化,可以看到,使用JavaPoet我们甚至不需要进行代码缩进,生成的代码会自动缩进!!!!

本文使用的代码均可通过github查看,项目传送门

参考文章