APT注解器的使用

Posted by Vane's Blog on February 7, 2020

1 APT简介

APT是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。简单来说,可以通过APT,根据规则,帮我们生成代码、生成类文件。ButterKnife、Dagger、EventBus等开源库都是利用注解实现的。

java是结构体语言,如下例子所示:

package com.vanelst.apt;//PackageElement包元素/节点
public class Main {//TypeElement类元素/节点
    private int x;//VariableElement属性元素/节点
    public void get(){}//ExecuteableElement属性/节点
 }
  • PackageElement

​ 表示一个包程序元素。提供对有关包及其成员的信息的访问

  • ExecuteableElement

​ 表示某个类或接口的方法、构造方法或初始化程序(静态或实例)

  • TypeElement

    表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问

  • VariableElement

    表示一个字段、enum常量、方法或构造方法参数、局部变量

2 APT常用API

属性名  
getEnclosedElements() 返回该元素直接包含的子元素
getEnclosingElement() 返回包含该element的父element,与上一个方法相反
getKind() 返回element的类型,判断是哪种element
getModifiers() 获取修饰关键字,如public等关键字
getSimpleName() 或取名字,不带包名
getQualifiedName() 获取全名,如果是类的话,包含完整的包名路径
getParameters() 获取方法的参数元素
getReturnType() 获取方法元素的返回值
getConstantValue() 获取属性变量被final修饰的值

3 APT实践

通过一个例子来讲解APT的使用:寻找activity类路径。

创建项目

  • 创建测试模块:Android Module命名为app
  • 创建自定义注解模块:Java library Module命名为 annotation
  • 创建注解处理器模块:Java library Module命名为 compiler 依赖 annotation

3.1 开发环境

Android Studio 3.3.2+Gradle 4.10.1(临界版本)

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

本文采用的版本:Android Studio 3.5.3+Gradle 5.4.1(向下兼容)

compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'

3.2 自定义注解模块annotation

代码如下:

package com.vanelst.annotation;

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

/**
 * <strong>Activity使用的布局文件注解</strong>
 * <ul>
 * <li>@Target(ElementType.TYPE)   // 接口、类、枚举、注解</li>
 * <li>@Target(ElementType.FIELD) // 属性、枚举的常量</li>
 * <li>@Target(ElementType.METHOD) // 方法</li>
 * <li>@Target(ElementType.PARAMETER) // 方法参数</li>
 * <li>@Target(ElementType.CONSTRUCTOR)  // 构造函数</li>
 * <li>@Target(ElementType.LOCAL_VARIABLE)// 局部变量</li>
 * <li>@Target(ElementType.ANNOTATION_TYPE)// 该注解使用在另一个注解上</li>
 * <li>@Target(ElementType.PACKAGE) // 包</li>
 * <li>@Retention(RetentionPolicy.RUNTIME) <br>注解会在class字节码文件中存在,jvm加载时可以通过反射获取到该注解的内容</li>
 * </ul>
 *
 * 生命周期:SOURCE < CLASS < RUNTIME
 * 1、一般如果需要在运行时去动态获取注解信息,用RUNTIME注解
 * 2、要在编译时进行一些预处理操作,如ButterKnife,用CLASS注解。注解会在class文件中存在,但是在运行时会被丢弃
 * 3、做一些检查性的操作,如@Override,用SOURCE源码注解。注解仅存在源码级别,在编译的时候丢弃该注解
 */
@Target(ElementType.TYPE)// 该注解作用在类之上
@Retention(RetentionPolicy.CLASS)// 要在编译时进行一些预处理操作。注解会在class文件中存在
public @interface ARouter {
    // 详细路由路径(必填),如:"/app/MainActivity"
    String path();

    // 路由组名(选填,如果开发者不填写,可以从path中截取出来)
    String group() default "";
}

3.3 注解处理器模块compiler

3.3.1 gradle配置

引入annotation模块和auto-service库

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
    // 帮助我们通过类调用的形式来生成Java代码
    implementation "com.squareup:javapoet:1.9.0"
    // 引入annotation,让注解处理器-处理注解
    implementation project(':annotation')
}

3.3.2 创建注解处理器类

package com.vanelst.compiler;

import com.google.auto.service.AutoService;
import com.vanelst.annotation.ARouter;

import java.io.IOException;
import java.io.Writer;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

/**
 * 注解处理器
 * @author WenYinghao
 * @date 2020-02-07
 * @Description
 */
// AutoService则是固定的写法,加个注解即可
// 通过auto-service中的@AutoService可以自动生成AutoService注解处理器,用来注册
// 用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.vanelst.annotation.ARouter"})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions("content")
public class ARouterProcessor extends AbstractProcessor {

    // 操作Element工具类 (类、函数、属性都是Element)
    private Elements elementUtils;

    // type(类信息)工具类,包含用于操作TypeMirror的工具方法
    private Types typeUtils;

    // Messager用来报告错误,警告和其他提示信息
    private Messager messager;

    // 文件生成器 类/资源,Filter用来创建新的源文件,class文件以及辅助文件
    private Filer filer;

    /**
     * 主要用于一些初始化的操作,通过该方法的参数ProcessingEnvironment可以获取一些列有用的工具类
     * @param processingEnvironment 当前或是之前的运行环境,可以通过该对象查找找到的注解。
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // 父类受保护属性,可以直接拿来使用。
        // 其实就是init方法的参数ProcessingEnvironment
        elementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();

        // 通过ProcessingEnvironment去获取build.gradle传过来的参数
        String content = processingEnvironment.getOptions().get("content");
        messager.printMessage(Diagnostic.Kind.NOTE, content);
    }

    /**
     * 相当于main函数,开始处理注解
     * 注解处理器的核心方法,处理具体的注解,生成Java文件
     * @param set 使用了支持处理注解的节点集合(类 上面写了注解)
     * @param roundEnvironment 当前或是之前的运行环境,可以通过该对象查找找到的注解
     * @return true 表示后续处理器不会再处理(已经处理完成)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.isEmpty()) return false;

        // 获取所有带ARouter注解的 类节点
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
        // 遍历所有类节点
        for (Element element : elements) {
            // 通过类节点获取包节点(全路径:com.vanelst.xxx)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
            // 获取简单类名
            String className = element.getSimpleName().toString();
            messager.printMessage(Diagnostic.Kind.NOTE, "被注解的类有:" + className);
            // 最终想生成的类文件名
            String finalClassName = className + "$ARouter";

            //传统写法
            //traditionalProcess(filer, element, packageName, finalClassName, className);
            //利用javapoet
            javapoetProcess(filer, element, packageName, finalClassName, className);

        }
        return true;
    }

    /**
     * 传统写法
     * @param filer
     * @param element
     * @param packageName
     * @param finalClassName
     * @param className
     */
    private void traditionalProcess(Filer filer, Element element, String packageName,
                                    String finalClassName, String className) {
        try {
            // 创建一个新的源文件(Class),并返回一个对象以允许写入它
            JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + finalClassName);
            // 定义Writer对象,开启写入
            Writer writer = sourceFile.openWriter();
            // 设置包名
            writer.write("package " + packageName + ";\n");
            writer.write("public class " + finalClassName + " {\n");
            writer.write("public static Class<?> findTargetClass(String path) {\n");
            // 获取类之上@ARouter注解的path值
            ARouter aRouter = element.getAnnotation(ARouter.class);
            writer.write("if (path.equals(\"" + aRouter.path() + "\")) {\n");
            writer.write("return " + className + ".class;\n}\n");
            writer.write("return null;\n");
            writer.write("}\n}");
            // 最后结束别忘了
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 高级写法,javapoet构建工具
     * @param filer
     * @param element
     * @param packageName
     * @param finalClassName
     * @param className
     */
    private void javapoetProcess(Filer filer, Element element, String packageName,
                                    String finalClassName, String className) {
        try {
            // 获取类之上@ARouter注解的path值
            ARouter aRouter = element.getAnnotation(ARouter.class);

            // 构建方法体
            MethodSpec method = MethodSpec.methodBuilder("findTargetClass") // 方法名
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(Class.class) // 返回值Class<?>
                    .addParameter(String.class, "path") // 参数(String path)
                    .addStatement("return path.equals($S) ? $T.class : null",
                            aRouter.path(), ClassName.get((TypeElement) element))
                    .build(); // 构建

            // 构建类
            TypeSpec type = TypeSpec.classBuilder(finalClassName)
                    .addModifiers(Modifier.PUBLIC) //, Modifier.FINAL)
                    .addMethod(method) // 添加方法体
                    .build(); // 构建

            // 在指定的包名下,生成Java类文件
            JavaFile javaFile = JavaFile.builder(packageName, type)
                    .build();
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

自定义ARouterProcessor类继承AbstractProcessor,该类主要是自定义注解的注解处理器,最主要的方法是process(),该方法是处理自定义注解的具体地方。该例子是利用注解获取activity的路径,传统的做法traditionalProcess是获取所有带ARouter注解的类节点,然后一条一条语句写入新的class文件中。开源库EventBus也是采用该方法,高级写法javapoetProcess是利用开源库javapoet,通过javapoet可以更加简单得生成这样的Java代码。

3.4 创建测试模块app

3.4.1 gradle环境

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.vanelst.aptdemo"
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        // 在gradle文件中配置选项参数值(用于APT传参接收)
        // 切记:必须写在defaultConfig节点下
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [content : 'hello apt']
            }
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    // 依赖自定义注解模块
    implementation project(':annotation')
    // 依赖注解处理器
    annotationProcessor project(':compiler')
}

3.4.2 测试Activity类

创建两个activity测试跳转。

package com.vanelst.aptdemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import com.vanelst.annotation.ARouter;

@ARouter(path = "/app/MainActivity")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void jump(View view) {
        Intent intent = new Intent(this, SecondActivity$ARouter.findTargetClass("/app/SecondActivity"));
        startActivity(intent);
    }
}
package com.vanelst.aptdemo;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

import com.vanelst.annotation.ARouter;

@ARouter(path = "/app/SecondActivity")
public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
    }

    public void jumpTo(View view) {
        Intent intent = new Intent(this, MainActivity$ARouter.findTargetClass("/app/MainActivity"));
        startActivity(intent);
    }
}

利用注解生成的activity类进行跳转。编译后,会在app模块的下面模块生成两个activity文件

app/build/generated/ap_generated_sources/debug/out/com/vanelst/aptdemo/MainActivity$ARouter.java
app/build/generated/ap_generated_sources/debug/out/com/vanelst/aptdemo/SecondActivity$ARouter.java

具体代码见:APTDemo源码