Android APT 系列 (三):APT 技术探究

APT 介绍
什么是 APT ?

APT 全称 Annotation Processing Tool,翻译过来即注解处理器。引用官方一段对 APT 的介绍:APT 是一种处理注释的工具, 它对源代码文件进行检测找出其中的注解,并使用注解进行额外的处理。

APT 有什么用?

APT 能在编译期根据编译阶段注解,给我们自动生成代码,简化使用。很多流行框架都使用到了APT技术,如 ButterKnifeRetrofitArouterEventBus 等等

APT 工程
1)、APT 工程创建

一般情况下,APT 大致的的一个实现过程:
1、创建一个Java Module ,用来编写注解
2、创建一个Java Module,用来读取注解信息,并根据指定规则,生成相应的类文件
3、创建一个Android Module ,通过反射获取生成的类,进行合理的封装,提供给上层调用
如下图:

示例图.png

这是我的 APT工程,关于 Module名称可以任意取,按照我上面说的规则去进行就好了

2)、Module 依赖

工程创建好后,我们就需要理清楚各个 Module 之间的一个依赖关系:

1、因为 apt-processor 要读取apt-annotation的注解,所以apt-processor需要依赖apt-annotation

//apt-processor 的 build.gradle 文件
dependencies {
    implementation project(path: ':apt-annotation')
}

2、app 作为调用层,以上 3 个 Module都需要进行依赖

//app 的 build.gradle 文件
dependencies {
    //...
    implementation project(path: ':apt-api')
    implementation project(path: ':apt-annotation')
    annotationProcessor project(path: ':apt-processor')
}

APT工程配置好之后,我们就可以对各个 Module 进行一个具体代码的编写了

apt-annotation 注解编写

这个 Module的处理相对来说很简单,就是编写相应的自定义注解就好了,我编写的如下:

@Inherited
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface AptAnnotation {
    String desc() default "";
}
apt-processor 自动生成代码

这个 Module 相对来说比较复杂,我们把它分为以下 3 个步骤:
1、注解处理器声明
2、注解处理器注册
3、注解处理器生成类文件

1)、注解处理器声明

1、新建一个类,类名按照自己的喜好取,继承javax.annotation.processing这个包下的 AbstractProcessor类并实现其抽象方法

public class AptAnnotationProcessor extends AbstractProcessor {
  
    /**
     * 编写生成 Java 类的相关逻辑
     *
     * @param set              支持处理的注解集合
     * @param roundEnvironment 通过该对象查找指定注解下的节点信息
     * @return true: 表示注解已处理,后续注解处理器无需再处理它们;false: 表示注解未处理,可能要求后续注解处理器处理
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

重点看下第一个参数中的 TypeElement ,这个就涉及到Element 的知识,我们简单的介绍一下:
Element 介绍
实际上,Java 源文件是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element,例如包,类,字段,方法等等:

package com.dream;         // PackageElement:包元素

public class Main<T> {     // TypeElement:类元素; 其中 <T> 属于 TypeParameterElement 泛型元素

    private int x;         // VariableElement:变量、枚举、方法参数元素

    public Main() {        // ExecuteableElement:构造函数、方法元素
    }
}

JavaElement是一个接口,源码如下:

public interface Element extends javax.lang.model.AnnotatedConstruct {
    // 获取元素的类型,实际的对象类型
    TypeMirror asType();
    // 获取Element的类型,判断是哪种Element
    ElementKind getKind();
    // 获取修饰符,如public static final等关键字
    Set<Modifier> getModifiers();
    // 获取类名
    Name getSimpleName();
    // 返回包含该节点的父节点,与getEnclosedElements()方法相反
    Element getEnclosingElement();
    // 返回该节点下直接包含的子节点,例如包节点下包含的类节点
    List<? extends Element> getEnclosedElements();

    @Override
    boolean equals(Object obj);
  
    @Override
    int hashCode();
  
    @Override
    List<? extends AnnotationMirror> getAnnotationMirrors();
  
    //获取注解
    @Override
    <A extends Annotation> A getAnnotation(Class<A> annotationType);
  
    <R, P> R accept(ElementVisitor<R, P> v, P p);
}

我们可以通过Element获取如上一些信息(写了注释的都是一些常用的)
Element衍生出来的扩展类共有 5 种:
1、PackageElement表示一个包程序元素
2、TypeElement表示一个类或者接口程序元素
3、TypeParameterElement 表示一个泛型元素
4、VariableElement表示一个字段、enum常量、方法或者构造方法的参数、局部变量或异常参数
5、ExecutableElement表示某个类或者接口的方法、构造方法或初始化程序(静态或者实例)
可以发现,Element 有时会代表多种元素,例如 TypeElement代表类或接口,此时我们可以通过 element.getKind()来区分:

Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);
for (Element element : elements) {
    if (element.getKind() == ElementKind.CLASS) {
        // 如果元素是类

    } else if (element.getKind() == ElementKind.INTERFACE) {
        // 如果元素是接口

    }
}

ElementKind是一个枚举类,它的取值有很多,如下:

PACKAGE //表示包
ENUM //表示枚举
CLASS //表示类
ANNOTATION_TYPE //表示注解
INTERFACE //表示接口
ENUM_CONSTANT //表示枚举常量
FIELD //表示字段
PARAMETER //表示参数
LOCAL_VARIABLE //表示本地变量
EXCEPTION_PARAMETER //表示异常参数
METHOD //表示方法
CONSTRUCTOR //表示构造函数
OTHER //表示其他

关于Element就介绍到这,我们接着往下看

2、重写方法解读
除了必须实现的这个抽象方法,我们还可以重写其他 4 个常用的方法,如下:

public class AptAnnotationProcessor extends AbstractProcessor {
    //...
  
    /** 
     * 节点工具类(类、函数、属性都是节点)
     */
    private Elements mElementUtils;

    /** 
     * 类信息工具类
     */
    private Types mTypeUtils;

    /**
     * 文件生成器
     */
    private Filer mFiler;

    /**
     * 日志信息打印器
     */
    private Messager mMessager;
  
    /**
     * 做一些初始化的工作
     * 
     * @param processingEnvironment 这个参数提供了若干工具类,供编写生成 Java 类时所使用
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
        mTypeUtils = processingEnv.getTypeUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }
  
    /**
     * 接收外来传入的参数,最常用的形式就是在 build.gradle 脚本文件里的 javaCompileOptions 的配置
     *
     * @return 属性的 Key 集合
     */
    @Override
    public Set<String> getSupportedOptions() {
        return super.getSupportedOptions();
    }

    /**
     * 当前注解处理器支持的注解集合,如果支持,就会调用 process 方法
     *
     * @return 支持的注解集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }
        
    /**
     * 编译当前注解处理器的 JDK 版本
     *
     * @return JDK 版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}

注意:getSupportedAnnotationTypes()getSupportedSourceVersion()getSupportedOptions()这三个方法,我们还可以采用注解的方式进行提供:

@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AptAnnotationProcessor extends AbstractProcessor {
    //...
}
2)、注解处理器注册

注解处理器声明好了,下一步我们就要注册它,其中注册有两种方式:
1、手动注册
2、自动注册
手动注册比较繁琐固定且容易出错,不推荐使用,这里就不讲了。我们主要看下自动注册

自动注册

1、首先我们要在apt-processor这个 Module 下的 build.gradle 文件导入如下依赖:

implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

注意:这两句必须都要加,否则注册不成功,我之前踩坑了

2、在注解处理器上加上 @AutoService(Processor.class) 即可完成注册

@AutoService(Processor.class)
public class AptAnnotationProcessor extends AbstractProcessor {
    //...
}
3)、注解处理器生成类文件

注册完成之后,我们就可以正式编写生成Java 类文件的代码了,其中生成也有两种方式:
1、常规的写文件方式
2、通过 javapoet 框架来编写
1 的方式比较死板,需要把每一个字母都写上,不推荐使用,这里就不讲了。我们主要看下通过 javapoet 这个框架生成Java 类文件

javapoet 方式

这种方式更加符合面向对象编码的一个风格,对 javapoet还不熟的朋友,可以去github 上学习一波 传送门,这里我们介绍一下它常用的一些类:
TypeSpec:用于生成类、接口、枚举对象的类
MethodSpec:用于生成方法对象的类
ParameterSpec:用于生成参数对象的类
AnnotationSpec:用于生成注解对象的类
FieldSpec:用于配置生成成员变量的类
ClassName:通过包名和类名生成的对象,在JavaPoet中相当于为其指定 Class
ParameterizedTypeName:通过 MainClassIncludeClass 生成包含泛型的Class
JavaFile:控制生成的 Java 文件的输出的类

1、导入 javapoet 框架依赖
implementation 'com.squareup:javapoet:1.13.0'
2、按照指定代码模版生成 Java 类文件

例如,我在appbuild.gradle下进行了如下配置:

android {
    //...
    defaultConfig {
        //...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [MODULE_NAME: project.getName()]
            }
        }
    }
}

MainActivity下面进行了如下注解:

@AptAnnotation(desc = "我是 MainActivity 上面的注解")
public class MainActivity extends AppCompatActivity {

    @AptAnnotation(desc = "我是 onCreate 上面的注解")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAptApi.init();
    }
}@AptAnnotation(desc = "我是 MainActivity 上面的注解")
public class MainActivity extends AppCompatActivity {

    @AptAnnotation(desc = "我是 onCreate 上面的注解")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAptApi.init();
    }
}

我希望生成的代码如下:

public class HelloWorld {
  public void test(String param) {
    System.out.println("模块: apt-app");
    System.out.println("节点: MainActivity  描述: 我是 MainActivity 上面的注解");
    System.out.println("节点: onCreate  描述: 我是 onCreate 上面的注解");
  }
}

现在我们来实操一下:

@AutoService(Processor.class)
@SupportedOptions("MODULE_NAME")
@SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AptAnnotationProcessor extends AbstractProcessor {
        
    //文件生成器
    Filer filer;
    //模块名
    private String mModuleName;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //初始化文件生成器
        filer = processingEnvironment.getFiler();
        //通过 key 获取 build.gradle 中对应的 value
        mModuleName = processingEnv.getOptions().get("MODULE_NAME");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set == null || set.isEmpty()) {
            return false;
        }
                
        //获取当前注解下的节点信息
        Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);

        // 构建 test 函数
        MethodSpec.Builder builder = MethodSpec.methodBuilder("test")
                .addModifiers(Modifier.PUBLIC) // 指定方法修饰符
                .returns(void.class) // 指定返回类型
                .addParameter(String.class, "param"); // 添加参数
        builder.addStatement("$T.out.println($S)", System.class, "模块: " + mModuleName);

        if (rootElements != null && !rootElements.isEmpty()) {
            for (Element element : rootElements) {
                //当前节点名称
                String elementName = element.getSimpleName().toString();
                //当前节点下注解的属性
                String desc = element.getAnnotation(AptAnnotation.class).desc();
                // 构建方法体
                builder.addStatement("$T.out.println($S)", System.class, 
                                     "节点: " + elementName + "  " + "描述: " + desc);
            }
        }
        MethodSpec main =builder.build();

        // 构建 HelloWorld 类
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC) // 指定类修饰符
                .addMethod(main) // 添加方法
                .build();

        // 指定包路径,构建文件体
        JavaFile javaFile = JavaFile.builder("com.dream.aptdemo", helloWorld).build();
        try {
            // 创建文件
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return true;
    }
}

经过上面这些步骤,我们运行App就能生成上面截图的代码了,现在还差最后一步,对生成的代码进行使用

注意:不同版本的 Gradle 生成的类文件位置可能不一样,我的Gradle 版本是 6.7.1,生成的类文件在如下位置:

示例图.png

一些低版本的 Gradle 生成的类文件在 /build/generated/source这个目录下

apt-api 调用生成代码完成业务功能

这个Module的操作相对来说也比较简单,就是通过反射获取到生成的类,进行相应的封装使用即可,我的编写如下:

public class MyAptApi {

    @SuppressWarnings("all")
    public static void init() {
        try {
            Class c = Class.forName("com.dream.aptdemo.HelloWorld");
            Constructor declaredConstructor = c.getDeclaredConstructor();
            Object o = declaredConstructor.newInstance();
            Method test = c.getDeclaredMethod("test", String.class);
            test.invoke(o, "");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接着我们在 MainActivityoncreate 方法里面进行调用:

@AptAnnotation(desc = "我是 MainActivity 上面的注解")
public class MainActivity extends AppCompatActivity {
  
    @AptAnnotation(desc = "我是 onCreate 上面的注解")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAptApi.init();
    }
}

//打印结果
模块:app
节点: MainActivity 描述: 我是MainActivity上面的注解
节点: onCreate 描述: 我是onCreate 上面的注解

总结

本篇文章讲的一些重点内容:
1、APT 工程所需创建的不同种类的 ModuleModule之间的依赖关系
2、Java源文件实际上是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element
3、采用 auto-service对注解处理器进行自动注册
4、采用 javapoet框架编写所需生成的Java类文件
5、通过反射及适当的封装,将生成的类的功能提供给上层调用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容