【Android】APT——注解处理器(一):初窥

在上一篇文章《注解实例 - 实现一个简单的@Autowired运行时注解》中,介绍了如何通过一个运行时注解来实现一个简单的依赖注入工具。
虽然使用方便,但是运行时注解是有一个硬伤的,那就是使用时需要进行大量扫描和反射操作,会对运行效率造成一定影响。同时,一些功能需要自动生成代码来提供,这时候,就需要用到APT了。当然,这里的APT指的不是信息安全中的APT,而是Annotation Processor Tool,即注解处理器工具。

本文将介绍如何写出一个最简单的APT demo,通过APT处理注解并自动生成文件,其中关于注解的知识就不再介绍了。

首先明确Demo目标:

  1. 创建一个类注解MyAnnotation以及自定义注解处理器MyProcessor
  2. 通过MyProcessor自动生成类文件,通过这个类中的函数可以打印出所有标注了MyAnnotation注解的类。

一、准备工作

1.1 新建工程模块

首先在Android Studio中新建工程。

新建App模块apt-app,为应用主模块。
新建Java lib模块apt-annotation,为自定义注解模块。
新建Java lib模块apt-processor,为自定义注解处理器模块。

1.2 添加依赖

apt-processor的build.gradle依赖中添加如下依赖:

    implementation "com.google.auto.service:auto-service:1.0-rc6"
    annotationProcessor "com.google.auto.service:auto-service:1.0-rc6"
    implementation 'com.squareup:javapoet:1.13.0'

    implementation project(":apt-annotation")

其中google auto service的作用是辅助自定义的注解处理器的注册。
javapoet的作用是自动生成代码。

apt-app模块中添加如下依赖:

    implementation project(":apt-annotation")
    annotationProcessor project(":apt-processor")

注意这行annotationProcessor project(":apt-processor"),是在Java中使用注解处理器;如果需要在kotlin中使用,则将annotationProcessor修改为kapt即可(同时需要在脚本文件最上方添加kapt插件)。

1.3 创建注解

这个注解不需要任何参数,只作为一个标记:

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
}

二、自定义注解处理器

2.1 创建自定义注解处理器类MyProcessor

在apt-processor模块新建继承自AbstractProcessor的类MyProcessor,内容如下:

@SupportedOptions("my_param")  // 接收外来参数的key
@SupportedAnnotationTypes("top.littlefogcat.apt_annotation.MyAnnotation") // 支持的注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 支持的Java版本
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> types, RoundEnvironment rEnv) {}
}

其中类上面三个注解依次代表了注解处理器接受外部参数的key(暂时没有用到)、注解处理器支持的注解类型(即1.3中创建的注解全名)、注解处理器支持的Java版本。这三个配置也可以通过重写类中的方法来实现,不过通过注解更加简便明了。

2.2 重写init方法

AbstractProcessorinit方法提供了一个环境对象pEnv,从这个对象中可以得到一系列的工具以及获取到外部传入的参数。这里通过pEnv.getFiler()获取到文件工具,以便之后创建文件。

    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment pEnv) {
        super.init(pEnv);
        mFiler = pEnv.getFiler();
    }

2.3 重写process方法

process是注解处理器的核心方法,需要在其中实现注解的处理。

2.3.1 前置

process方法的参数

process方法有两个参数:Set<? extends TypeElement> typesRoundEnvironment rEnv。其中前者表示了需要处理的注解的集合,即创建自定义注解处理器时SupportedAnnotationTypes中所定义的类;后者则是APT框架提供的查询程序元素的工具,如通过rEnv.getElementsAnnotatedWith可以查询到程序中所有标注了某注解的类。

Element

众所周知,对于静态的Java语言(源文件级别),是由包、类、方法等程序元素组成的;在对Java源码的处理中,各种程序元素对应了javax.lang.model.element.Element接口。这个概念在之后的处理中会用到。

javapoet

javapoet是一个辅助自动生成java代码的工具,可以方便的生成代码。其中关键类包括:JavaFile(对应.java文件)、TypeSpec(对应类)、MethodSpec(对应方法)、FieldSpec(对应成员变量)、ParameterSpec(对应参数)、AnnotationSpec(对应注解)等。之后会使用javapoet来生成代码。

2.3.2 通过javapoet生成方法

明确目标

首先确定需要生成方法的格式。目标是这样的,即打印所有标注了@MyAnnotation的类的名称:

  public void print() {
    System.out.println("以下是标注了@MyAnnotation注解的类");
    System.out.println("Class1");
    System.out.println("Class2");
    System.out.println("Class3");
  }

创建method builder

在javapoet中,方法对应的类是MethodSpec。首先通过建造者模式来创建一个builder同时指定方法名、通过addModifiers指定可见性、通过returns指定返回值类型:

MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("print") // 函数名
                .addModifiers(Modifier.PUBLIC) // 添加限定符
                .returns(void.class); // 返回类型

通过addStatement方法,可以在方法体中添加语句。这里打印一句话“以下是标注了@MyAnnotation注解的类”

methodBuilder.addStatement("$T.out.println(\"以下是标注了@MyAnnotation注解的类\")", System.class); // 添加语句

扫描所有标注了@MyAnnotation的类并打印出来

在2.3.1中已经介绍到,可以通过process方法的第二个参数获取所有标注了某个标记的类。然后再在方法中添加打印语句,将这些类的名称打印出来:

    // 标注了@MyAnnotation的节点
    Set<? extends Element> rootElements = rEnv.getElementsAnnotatedWith(MyAnnotation.class);
    // 查询所有标注了@MyAnnotation的类,并打印出来
    if (rootElements != null && !rootElements.isEmpty()) {
        for (Element element : rootElements) {
            String name = element.getSimpleName().toString();
            methodBuilder.addStatement("$T.out.println($S)", System.class, name); // 添加打印语句
        }
    }

完成构造

    MethodSpec method = methodBuilder.build(); // 完成构造

到这里,目标方法就已经构建完成了。

2.3.3 通过javapoet生成类

与生成方法类似,生成类也是通过构造者模式,并可以通过addMethod将之前生成的方法添加到这个类中:

    // 生成类
    TypeSpec myClass = TypeSpec.classBuilder("AptGeneratedClass") // 类名
            .addModifiers(Modifier.PUBLIC) // public类
            .addMethod(method) // 添加上述方法
            .build(); // 构造类

这里将生成的类名命名为AptGeneratedClass

2.3.4 生成Java文件

生成Java文件分为两步,第一步是通过2.3.3中的类对象生成JavaFile类型的文件对象,第二步是通过2.2中获取的Filer文件工具将其写入到.java文件。

    // 生成文件
    JavaFile javaFile = JavaFile.builder("top.littlefogcat.apt", myClass) // 包名、类对象
            .build();
    try {
        javaFile.writeTo(mFiler); // 通过文件工具创建文件
    } catch (IOException e) {
        e.printStackTrace();
    }

至此,就通过APT完成了一个最简单的可以自动生成文件的注解处理器。

2.4 包含注释的MyProcessor完整代码

/**
 * 自定义APT类
 * <p>
 * TypeElement:类元素
 * <p>
 * 对于Java语言来讲,将其看做结构化的语言模型,那么就分为了:
 * PackageElement包元素,
 * TypeElement类元素,
 * TypeParameterElement泛型元素,
 * VariableElement变量元素
 * ExecutableElement可执行元素(方法)
 * <p>
 * 见{@link javax.lang.model.element.Element}
 */
@AutoService(Processor.class)
@SupportedOptions("my_param")  // 接收外来参数的key
@SupportedAnnotationTypes("top.littlefogcat.apt_annotation.MyAnnotation") // 支持的注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 支持的Java版本
public class MyProcessor extends AbstractProcessor {
    /*
     * 一些工具,在init方法中通过环境对象获取
     */
    private Types mTypeUtils;
    private Elements mElementUtils;
    private Messager mMessager;
    private Filer mFiler; // 文件工具

    private String mParam;

    /**
     * 做一些初始化的工作,可以通过pEnv参数获取一些工具类。
     * 同时,通过`SupportedOptions`配置的参数也可以在这里获取。
     *
     * @param pEnv 环境对象,提供一些工具
     */
    @Override
    public synchronized void init(ProcessingEnvironment pEnv) {
        super.init(pEnv);
        mTypeUtils = pEnv.getTypeUtils();
        mElementUtils = pEnv.getElementUtils();
        mMessager = pEnv.getMessager();
        mFiler = pEnv.getFiler();

        mParam = pEnv.getOptions().get("env_param"); // 获取外界传入参数
    }

    /**
     * @param types 需要处理的注解集合
     * @param rEnv  运行环境?通过这个对象查询节点信息
     * @return 处理成功返回true,否则返回false
     */
    @Override
    public boolean process(Set<? extends TypeElement> types, RoundEnvironment rEnv) {
        if (types == null || types.isEmpty()) {
            return false;
        }


        // 生成一个函数格式如:
        // public void print() {
        //     System.out.println("以下是标注了@MyAnnotation注解的类");
        //     System.out.println("AnnotedClass1");
        //     System.out.println("AnnotedClass2");
        // }
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("print") // 函数名
                .addModifiers(Modifier.PUBLIC) // 添加限定符
                .returns(void.class) // 返回类型
                .addStatement("$T.out.println(\"以下是标注了@MyAnnotation注解的类\")", System.class); // 添加语句

        // 标注了@MyAnnotation的节点
        Set<? extends Element> rootElements = rEnv.getElementsAnnotatedWith(MyAnnotation.class);
        // 查询所有标注了@MyAnnotation的类,并打印出来
        if (rootElements != null && !rootElements.isEmpty()) {
            for (Element element : rootElements) {
                String name = element.getSimpleName().toString();
                methodBuilder.addStatement("$T.out.println($S)", System.class, name);
            }
        }
        MethodSpec method = methodBuilder.build(); // 完成构造

        // 生成类
        TypeSpec myClass = TypeSpec.classBuilder("AptGeneratedClass") // 类名
                .addModifiers(Modifier.PUBLIC) // public类
                .addMethod(method) // 添加上述方法
                .build(); // 构造类

        // 生成文件
        JavaFile javaFile = JavaFile.builder("top.littlefogcat.apt", myClass) // 包名、类型
                .build();
        try {
            javaFile.writeTo(mFiler); // 通过文件工具创建文件
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 接受外来参数,比如在build.gradle中的javaCompileOptions.annotationProcessorOptions配置
     * <p>
     * 也可以通过`@SupportedOptions`注解来配置
     */
    @Override
    public Set<String> getSupportedOptions() {
        return Collections.singleton("my_param");
    }

    /**
     * 返回当前注解处理器支持的注解类型
     * <p>
     * 也可以通过`@SupportedAnnotationTypes`注解来配置
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton("top.littlefogcat.apt_annotation.MyAnnotation");
    }

    /**
     * 返回当前注解处理器支持的JDK版本
     * <p>
     * 也可以通过`@SupportedSourceVersion`注解来配置
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }

}

三、测试效果

apt-app模块中创建MainActivity,将其添加@MyAnnotation注解。这里使用的kotlin,注意build.gradle中需要将annotationProcessor改成kapt

@MyAnnotation
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Build一下,可以看到AptGeneratedClass.java已经生成完毕了,并且打印出了标注了@MyAnnotation注解的类(MainActivity)。

生成文件

生成文件

四、参考资料

《Android APT 系列 (三):APT 技术探究》
《JavaPoet - 优雅地生成代码》

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

推荐阅读更多精彩内容