Android 利用 APT 技术在编译期生成代码

澳门新葡亰手机版 5

APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的
Java 文件,减少手动的代码输入。现在有很多主流库都用上了 APT,比如
Dagger2, ButterKnife, EventBus3 等,我们要紧跟潮流,与时俱进呐! (ง
•̀_•́)ง

前言

在前面的文章中,咱们学习了Java类加载、Java反射、Java注解,那现在咱们就可以利用所学搞点事情了,所谓学以致用,方为正途。

如果想直接阅读源码,请点这里Github

下面通过一个简单的 View 注入项目 ViewFinder 来介绍 APT
相关内容,简单实现了类似于ButterKnife 中的两种注解 @BindView 和 @OnClick 。

铺垫

在开始搞事情前,咱们还需要了解以下几个物件:

  • Annotation Processor: 注解处理器
  • JavaPoet:Java源码文件生成者
  • javax.lang.model.element:用于解析程序中的元素,例如:包、类、方法、变量

项目地址:

Annotation Processor

注解处理器是在编译时用来扫描和处理注解的工具。你可以注册自己感兴趣的注解,程式编译时会将添加注解的元素,交由注册它的注解处理器来处理。

那咱们如何实现一个自己的注解处理器?

  1. 继承AbstractProcessor
  2. 覆盖getSupportedAnnotationTypes()
  3. 覆盖getSupportedSourceVersion()
  4. 覆盖process()

AbstractProcessor:抽象注释处理器,为大多数自定义注释处理器的超类。

getSupportedAnnotationTypes():这里注册你感兴趣的注解。它的返回一个字符串的Set,包含注解类型的合法全称。

getSupportedSourceVersion():指定使用的Java版本。通常这里返回SourceVersion.latestSupported()。

process(Set<? extends TypeElement> set, RoundEnvironment
roundEnvironment):注解处理器的核心方法,在这里进行注解扫描、评估和处理,以及生成Java文件。

生成Java文件,就交由JavaPoet来完成

大概项目结构如下:

JavaPoet

JavaPoet是一个用来生成
.java源文件的工具(由Square提供)。

咱们来讲一下JavaPoet里面常用的几个类:

  • TypeSpec:表示一个类、接口或者枚举声明
  • MethodSpec:表示一个构造函数或方法声明
  • FieldSpec:表示一个成员变量、字段声明
  • JavaFile:生成java文件

下面通过一个实例来说明具体使用方式:

private void generateHelloWorld() throws IOException {
        MethodSpec mainMethod = MethodSpec.methodBuilder("main")
                .addModifiers(new Modifier[]{Modifier.PUBLIC, Modifier.STATIC})
                .addParameter(String[].class, "args")
                .addStatement("System.out.println("Hello World")")
                .build();

        FieldSpec androidVersion = FieldSpec.builder(String.class, "androidVer")
                .addModifiers(new Modifier[]{Modifier.PRIVATE})
                .initializer("$S", "Lollipop")
                .build();

        TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(new Modifier[]{ Modifier.FINAL, Modifier.PUBLIC})
                .addMethod(mainMethod)
                .addField(androidVersion)
                .build();

        JavaFile javaFile = JavaFile.builder("com.hys.test", typeSpec).build();
        javaFile.writeTo(System.out);
    }

执行函数,结果如下:

package com.hys.test;

import java.lang.String;

public class HelloWorld {
    private String androidVer = "Lollipop";

    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

这里$S占位符,JavaPoet占位符如下:

  • $S:字符串类型占位符
  • $T:类型占位符
  • $N:名称占位符(方法名或者变量名等)
  • $L:字面常量

这里只是投石问路,关于JavaPoet更多API使用,请参见其文档

  • viewFinder-annotation – 注解相关模块
  • viewFinder-compiler – 注解处理器模块
  • viewfinder – API 相关模块
  • sample – 示例 Demo 模块

javax.lang.model.element

Element
用于 Java 的模型元素的接口。

  • ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素
  • PackageElement:表示一个包程序元素
  • TypeElement:表示一个类或接口程序元素
  • TypeParameterElement:表示类、接口、方法或构造方法元素的形式类型参数
  • VariableElement:表示一个字段、enum
    常量、方法或构造方法参数、局部变量或异常参数

通过Element的getModifiers()获得元素的修饰符

Modifier
表示程序元素(如类、方法或字段)上的修饰符。
以下是常用修饰符:

  • ABSTRACT:修饰符 abstract
  • FINAL:修饰符 final
  • NATIVE:修饰符 native
  • PRIVATE:修饰符 private
  • PROTECTED:修饰符 protected
  • PUBLIC:修饰符 public
  • STATIC:修饰符 static
  • SYNCHRONIZED:修饰符 synchronized

通过Element的asType()获得元素的类型

TypeMirror
表示 Java
编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和
null 类型。

通过TypeMirror的getKind()类型的种类

TypeKind
澳门新葡亰手机版,表示类型的种类。

以下是常用的类型:

  • ARRAY:数组类型
  • BOOLEAN:基本类型 boolean
  • BYTE:基本类型 byte
  • CHAR:基本类型 char
  • DECLARED:类或接口类型
  • DOUBLE:基本类型 double
  • ERROR:无法解析的类或接口类型。
  • EXECUTABLE:方法、构造方法或初始化程序
  • FLOAT:基本类型 float
  • INT:基本类型 int
  • LONG:基本类型 long
  • NONE:在实际类型不适合的地方使用的伪类型
  • NULL:null 类型
  • PACKAGE:对应于包元素的伪类型
  • SHORT:基本类型 short
  • TYPEVAR:类型变量
  • VOID:对应于关键字 void 的伪类型

获取元素的父元素
通过Element的getEnclosingElement返回元素的父元素。

获取元素上的注解
通过Element的getAnnotation(Class<A>
annotationType)获得元素上的注解。

了解了上述内容,下面咱们开始搞事情

实现目标

在通常的 Android
项目中,会写大量的界面,那么就会经常重复地写一些代码,比如:

TextView text = (TextView) findViewById(R.id.tv);
text.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // on click
    }
});

天天写这么冗长又无脑的代码,还能不能愉快地玩耍啦。所以,我打算通过 ViewFinder 这个项目替代这重复的工作,只需要简单地标注上注解即可。通过控件
id
进行注解,并且@OnClick 可以对多个控件注解同一个方法。就像下面这样子咯:

@BindView(R.id.tv) TextView mTextView;
@OnClick({R.id.tv, R.id.btn})
public void onSomethingClick() {
    // on click
}

创建注解处理器

1.Android Studio的File->New->New module,如下图:

澳门新葡亰手机版 1

2.在弹出的Create New Module对话框中选择Java
Library,命名为MockButterknife-complier,如下图:

澳门新葡亰手机版 2

3.创建注解处理器类,继承AbstractProcessor,覆盖getSupportedAnnotationTypes()、getSupportedSourceVersion()、process()三个方法,如下图:

澳门新葡亰手机版 3

4.注册注解处理器,在项目下创建resources->META-INF->Services目录,在Services目录下创建javax.annotation.processing.Processor文件,如下图:

澳门新葡亰手机版 4

5.编辑javax.annotation.processing.Processor文件,添加注解处理器类,如下图:

澳门新葡亰手机版 5

6.配置注解处理器,添加JavaPoet,如下:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.squareup:javapoet:1.8.0'
}

7.创建自定义注解,咱们在这里创建两个注解:

  • BindView注解

package com.hys.mockbutterknife.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
  • OnClick注解

package com.hys.mockbutterknife.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
    int[] value();

8.注册自定义注解到注解处理器,在AnnotationProcessor添加如下代码:

private Set<Class<? extends Annotation>> getSupportedAnnotations(){
        Set<Class<? extends Annotation>> supportedAnnotations = new LinkedHashSet<>();
        supportedAnnotations.add(BindView.class);
        supportedAnnotations.add(OnClick.class);
        return supportedAnnotations;
    }

在getSupportedAnnotationTypes()方法中调用getSupportedAnnotations(),即将自定义注解注册到注解处理器,代码如下:

 @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedAnnotationTypes = new LinkedHashSet<>();

        Iterator ite = getSupportedAnnotations().iterator();
        while (ite.hasNext()){
            Class annotation = (Class<? extends Annotation>)ite.next();
            supportedAnnotationTypes.add(annotation.getCanonicalName());
        }

        return supportedAnnotationTypes;
    }

9.上面咱们已经注册了自定义注解,接下来应该处理这些注解(啰嗦,不处理,注册它们做啥?!)

后面以BindView为例

查找添加注解的元素

Iterator ite = env.getElementsAnnotatedWith(BindView.class).iterator();

验证元素合法性

  • 验证元素是否可以访问

private boolean isInaccessible(Element element, String targetThing, Class<? extends Annotation> annotationClass) {

        TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
        //检查元素的访问修饰符
        Set<Modifier> modifiers = element.getModifiers();
        if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
            this.error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        //检查元素的父元素
        if (enclosingElement.getKind() != ElementKind.CLASS) {
            this.error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        //检查父元素的访问修饰符
        if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
            this.error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
            return true;
        }

        return false;
    }
  • 验证元素所在包的合法性

private boolean isInWrongPackage(Element element, Class<? extends Annotation> annotationClass) {

        TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
        String qualifiedName = enclosingElement.getQualifiedName().toString();
        //元素的父元素(即元素所在的类)不能在android的系统包中
        if (qualifiedName.startsWith("android.")) {
            this.error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", annotationClass.getSimpleName(), qualifiedName);
            return true;
        } 
        ////元素的父元素不能在java的资源包中
        else if (qualifiedName.startsWith("java.")) {
            this.error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", annotationClass.getSimpleName(), qualifiedName);
            return true;
        }

        return false;
    }
  • 验证元素类型的合法性

/*
* 递归验证
* 以TextView为例:isSubtypeOfType(typeMirror, "android.view.View")
*/
public static boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
        // 类型相同
        if (isTypeEqual(typeMirror, otherType))
            return true;

        if (typeMirror.getKind() != TypeKind.DECLARED)
            return false;

        DeclaredType declaredType = (DeclaredType)typeMirror;
        List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
        if (typeArguments.size() > 0) {
            StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
            typeString.append('<');

            for(int i = 0; i < typeArguments.size(); ++i) {
                if (i > 0) {
                    typeString.append(',');
                }

                typeString.append('?');
            }

            typeString.append('>');
            if (typeString.toString().equals(otherType)) {
                return true;
            }
        }

        Element element = declaredType.asElement();
        if (!(element instanceof TypeElement)) {
            return false;
        } else {
            TypeElement typeElement = (TypeElement)element;
            // 获取元素的父类
            TypeMirror superType = typeElement.getSuperclass();
            // 检查父类的类型
            if (isSubtypeOfType(superType, otherType)) {
                return true;
            } else {                
                Iterator var7 = typeElement.getInterfaces().iterator();

                TypeMirror interfaceType;
                do {
                    if (!var7.hasNext()) {
                        return false;
                    }

                    interfaceType = (TypeMirror)var7.next();
                } while(!isSubtypeOfType(interfaceType, otherType));

                return true;
            }
        }

    }

生成Java源文件

  • 生成类

private TypeSpec createTypeSpec(){
        // 生成新类名,原类名+ _ViewBinding
        String className = this.encloseingElement.getSimpleName().toString() + "_ViewBinding";
        // 获取父元素的类型全称
        TypeName targetTypeName = TypeName.get(this.encloseingElement.asType());

        // 创建类构建器
        TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
                .addModifiers(new Modifier[]{Modifier.PUBLIC}) // 添加public修饰符
                .addField(targetTypeName, "target", new Modifier[]{Modifier.PRIVATE}); // 添加成员变量target

        classBuilder.addFields(createFieldForListener());

        if(isActivity()){
            classBuilder.addMethod(createConstructorForActivity());
        } else if(isView()){
            classBuilder.addMethod(createConstructorForView());
        } else if(isDialog()){
            classBuilder.addMethod(createConstructorForDialog());
        }

        // 默认类构造器
        classBuilder.addMethod(createBindConstructor());
        // 生成类
        return classBuilder.build();
    }
  • 生成JavaFile对象

public JavaFile brewJava() {
        String packageName = MoreElements.getPackage(this.encloseingElement).getQualifiedName().toString();
        return JavaFile.builder(packageName, createTypeSpec()).build();
    }
  • 生成Java源文件

...
JavaFile javaFile = bindSet.brewJava();

try{
      javaFile.writeTo(this.processingEnv.getFiler());
}catch (IOException ex){
     this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, ex.getMessage());
}
...