编译时注解(APT) — 自定义注解处理器

关于编译时注解(APT)由浅入深有三部分,分别是:

1. 自定义注解处理器
例如 ButterKnife、Room 根据注解生成新的类。

2. 利用JcTree在编译时修改代码
像 Lombok 自动往类中新增 getter/setter 方法、往方法中插入代码行等。
这种方式不推荐使用,因为只对 Java 代码有效,对 Kotlin 代码无效。

3. 自定义 Gradle 插件在编译时修改代码
例如一些代码插桩框架、日志框架、方法耗时统计框架等。

这篇文章以Demo的形式,介绍如何从零开始创建一个自定义的注解处理器,并生成一个新的类。这个类中有一个静态方法,方法返回添加了自定义注解的所有类。 看懂这篇文章,你就能写出自己的 ButterKnife 啦~

本文中的源代码可以在这里查看: https://github.com/Sino-Snack/APT-Source-Code


1. 环境搭建和 Gradle 配置

1.1 创建注解 Module
我们在工程中新建一个 Java Library,Module 名称定义为 Annotation。再定义一个自定义的注解类:

@Target(ElementType.TYPE)
public @interface DemoAnnotation {
}

第一步就完啦~ (如果不清楚元注解的使用,可以搜索其它文章了解)

1.2 创建注解处理器 Module
在工程中再创建一个 Java Library,名称定义为 AnnotationProcessor,并在 build.gradle 中加入如下依赖:

import org.gradle.internal.jvm.Jvm

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // 刚才定义的 Annotation 模块
    implementation project(":Annotation")

    // 谷歌的 AutoService 可以让我们的注解处理器自动注册上
    implementation 'com.google.auto.service:auto-service:1.0-rc4'

    // 用于生成新的类、函数
    implementation "com.squareup:javapoet:1.9.0"

    // 谷歌的一个工具类库
    implementation "com.google.guava:guava:24.1-jre"

    implementation files(Jvm.current().toolsJar)
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

1.3 配置项目级的 build.gradle
再在项目级的 build.gradle 中增加 android-apt 的依赖:

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


2. 实现自定义注解处理器

所有的自定义注解处理器都应该继承自 AbstractProcessor 类。
我们也定义一个处理器,并实现几个模板方法:

@AutoService(Processor.class)
public class DemoProcessor extends AbstractProcessor {

    /* ======================================================= */
    /* Fields                                                  */
    /* ======================================================= */

    /**
     * 用于将创建的类写入到文件
     */
    private Filer mFiler;


    /* ======================================================= */
    /* Override/Implements Methods                             */
    /* ======================================================= */

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 这个方法是注解处理器的核心,稍后单独分析这个方法如何实现
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 这个方法返回当前处理器 能处理哪些注解,这里我们只返回 DemoAnnotation
        return Collections.singleton(DemoAnnotation.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        // 这个方法返回当前处理器 支持的代码版本
        return SourceVersion.latestSupported();
    }
}

2.1 process() 方法详解
我们的需求是生成一个新的类,类中有一个静态方法,方法返回添加了 @Annotation 注解的所有类。这些操作都需要我们在 process() 方法中去实现。步骤:
(1) 获取所有添加了注解的元素;
(2) 生成一个方法,方法的代码块是返回(1)中获取到的列表。
(3) 生成一个类,类中加入(2)中生成的方法;
(4) 将(3)中生成的类写入文件。

所以我们得到这个方法的实现:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {

    // 获取所有被 @DemoAnnotation 注解的类
    Set<? extends Element> elements = environment.getElementsAnnotatedWith(DemoAnnotation.class);

    // 创建一个方法,返回 Set<Class>
    MethodSpec method = createMethodWithElements(elements);

    // 创建一个类
    TypeSpec clazz = createClassWithMethod(method);

    // 将这个类写入文件
    writeClassToFile(clazz);

    return false;
}

接下来就让我们看看这三个关键的方法分别是怎么实现的:

2.2 如何创建新的方法

/**
 * 创建一个方法,这个方法返回 elements 中的所有类信息。
 */
private MethodSpec createMethodWithElements(Set<? extends Element> elements) {

    // "getAllClasses" 是生成的方法的名称
    MethodSpec.Builder builder = MethodSpec.methodBuilder("getAllClasses");

    // 为这个方法加上 "public static" 的修饰符
    builder.addModifiers(Modifier.PUBLIC, Modifier.STATIC);

    // 定义返回值类型为 Set<Class>
    ParameterizedTypeName returnType = ParameterizedTypeName.get(
            ClassName.get(Set.class),
            ClassName.get(Class.class)
    );
    builder.returns(returnType);

    // 经过上面的步骤,
    // 我们得到了 public static Set<Class> getAllClasses() {} 这个方法,
    // 接下来我们实现它的方法体:

    // 方法中的第一行: Set<Class> set = new HashSet<>();
    builder.addStatement("$T<$T> set = new $T<>();", Set.class, Class.class, HashSet.class);
    
    // 上面的 "$T" 是占位符,代表一个类型,可以自动 import 包。其它占位符:
    // $L: 字符(Literals)、 $S: 字符串(String)、 $N: 命名(Names)

    // 遍历 elements, 添加代码行
    for (Element element : elements) {

        // 因为 @Annotation 只能添加在类上,所以这里直接强转为 ClassType
        ClassType type = (ClassType) element.asType();

        // 在我们创建的方法中,新增一行代码: set.add(XXX.class);
        builder.addStatement("set.add($T.class)", type);
    }

    // 经过上面的 for 循环,我们就把所有添加了注解的类加入到 set 变量中了,
    // 最后,只需要把这个 set 作为返回值 return 就好了:
    builder.addStatement("return set");

    return builder.build();
}

2.3 如何创建新的类

/**
 * 创建一个类,并把参数中的方法加入到这个类中
 */
private TypeSpec createClassWithMethod(MethodSpec method) {
    // 定义一个名字叫 OurClass 的类
    TypeSpec.Builder ourClass = TypeSpec.classBuilder("OurClass");

    // 声明为 public
    ourClass.addModifiers(Modifier.PUBLIC);

    // 为这个类加入一段注释
    ourClass.addJavadoc("这个类是自动创建的哦~\n\n @author ZhengHaiPeng");

    // 为这个类新增一个方法
    ourClass.addMethod(method);

    return ourClass.build();
}

2.4 如何将创建的类写入文件

/**
 * 将一个创建好的类写入到文件中参与编译
 */
private void writeClassToFile(TypeSpec clazz) {
    // 声明一个文件在 "me.moolv.apt" 下
    JavaFile file = JavaFile.builder("me.moolv.apt", clazz).build();

    // 写入文件
    try {
        file.writeTo(mFiler);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

3. 使用自定义注解处理器

在要使用的 Module 中,例如 app,的 build.gradle 中加入依赖:

apply plugin: 'com.android.application'

android {
    ...
}

dependencies {
    ...
    annotationProcessor project(":AnnotationProcessor")
    implementation project(path: ':Annotation')
}

执行 Android Studio 的 Build > Make Project, 就能在 app Module 的 build/source/apt 路径下找到生成的类文件了:

/**
 * 这个类是自动创建的哦~
 *
 * @author ZhengHaiPeng
 */
public class OurClass {
    public static Set<Class> getAllClasses() {
        Set<Class> set = new HashSet<>();
        set.add(MainActivity.class);
        return set;
    }
}

这样我们就实现了 自定义注解处理器,并生成代码啦,有疑问留言就好~


4. 如何为注解处理器传递参数?

APT 中的 Processor 可能会用到一些参数,这些参数可以在 gradle 中配置。

设置参数

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
        
            annotationProcessorOptions {
                // 下面定义要传递的参数
                argument "key1", "value1"
                argument "key2", "value2"
            }
        }
    }

获取参数
在 Processor 的 init 方法中可以获取参数:

@Override
public synchronized void init(ProcessingEnvironment env) {
    super.init(env);
    
    ...

    String value1 = env.getOptions().get("key1");
    
    ...
}

源码:https://github.com/Sino-Snack/APT-Source-Code

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

推荐阅读更多精彩内容