Android - 没有比这更新鲜的注解教程了 AS3.4 Gradle5

Pixabay License

网上有很多 APT 相关教程,最近开始学这个,发现有一些内容已经过时了,在使用过程中也发现了一些坑,总结一下,形成这篇教程。

本文开发环境:2019年5月初最新版本的 Android Studio 3.4、Android Plugin 3.4.0、Gradle 5.1.1。

本教程需要读者了解注解 Annotation 的基本知识,不涉及 Annotation 运行时反射的用法,专注于自定义 APT 的流程和步骤,以及使用新版 AS 和 gradle 的注意事项。

简介

APT,Annotation Processing Tool,注解处理工具,是 JDK 提供的一个工具。注意是 Java 语言支持的,不是安卓特有的东西,这点对于理解 APT 有一些作用。早期的 JDK 提供了一个单独的 apt 程序,后来被整合到 javac 中了。它最常见的用法就是根据注解自动生成源代码,很多流行的库都使用了注解处理器来生成代码,比如 ButterKnife 会生成资源与变量绑定的代码,让开发者不用手写繁琐重复的 findViewById。

原理

那么 javac 是怎么使用 APT 生成代码的呢?javac 并不知道你想怎么生成代码,需要你按照 javac 提供的规则和接口来自定义 Annotation Processor。注意这是 Java 语言定义的规则。

接口

javax.annotation.processing.AbstractProcessor,实现这个抽象类,在 process() 方法中自定义生成代码的细节。可以称它为注解处理器 Annotation Processor。只有这一个类型作为接口,当然类中还有一些其他方法用来设置 Annotation Processor 的属性。

规则

  • 一个 Annotation Processor 想要参与到 javac 的编译过程中,就要被编译打包成一个 jar 文件。
  • 这个 jar 文件要放置在编译期的 classpath 中,javac 会自动查找 classpath 中所有的 Annotation Processor,自动完成注解处理。然而新版的 gradle 5 不再将 Annotation Processor 放在编译期的 classpath 中,导致还需要额外处理,这个是 gradle 的行为,处理方法见下文。
  • 这个 jar 文件中包含一个 META-INF/service/javax.annotation.processing.Processor 文件,文件内容是文本,每行一个 Annotation Processor 的完整类名称。

安卓和 gradle

以上是 Java 的基础规则,到了 gradle 中就要按照 Java Plugin 的语法和规则来配置。gradle 一直致力于提高编译速度,在新版的 gradle 5.+ 中,为了推行更快速的增量编译,关闭了一个默认功能,导致由 gradle 4.+ 升级上来的项目有可能构建失败,这其中的弯弯绕绕和坑坑洼洼在下面的步骤中详细讲解。

步骤

1. 项目架构

分 3 个模块:

  • annotation 模块:用来定义注解。
  • compiler 模块:用来定义 Annotation Processor。
  • app 模块:使用注解的应用模块。

为什么要分这么多模块?其中 app 模块是用来测试的,测试新定义的 Annotation Processor 能否成功运行。annotation + compiler 如果写得糙一点可以合并在一起,例如谷歌的 auto service。但两者的目的并不一样,annotation 是专门定义注解的,而 compiler 是处理注解的。最重要的是,annotation(RetentionPolicy.CLASS,RetentionPolicy.RUNTIME)是需要被编译到 app 项目的 class 文件中的,而 compiler 没有必要进入 app 中。

2. annotation 模块

该模块是定义注解用的,可以包含多个注解,例如 ButterKnife 就有二十多个注解定义。而且只有注解的定义,没有其他任何代码。这样做的原因主要是在架构上能单独隔离一个完整内聚的功能,可以被其他模块引用,比如 app 模块必须引用,compiler 模块可以引用。

创建模块

annotation 模块中只有注解定义,可以直接定义为 java library,而不用定义为 android library,定义为安卓库反而会限制它被其他 java library 引用。

build.gradle

apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

然后就可以在这个模块中添加注解定义了。

注解简介

定义注解使用 @interface 关键字,然后使用元注解 @Target@Retention 定义注解的修饰目标和保留策略:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface NiceField {}
  • @Target 指定和约束注解能修饰的代码元素,可以是类、字段、方法、参数等等。
  • @Retention 指定保留策略,指的是被修饰的代码,在编译后是否仍保留注解的策略,有三种:
    • RetentionPolicy.SOURCE:不做任何保留,编译之后就抛弃。
    • RetentionPolicy.CLASS:保留在 class 文件中,但运行时无法使用。
    • RetentionPolicy.RUNTIME:保留在 class 文件中,运行时可以通过反射使用。

当注解被 Annotation Processor 处理的时候,其实这三种策略的注解都是可以处理的。比如谷歌的 auto service 中的 @AutoService 就是 RetentionPolicy.SOURCE 类型的,用完即抛。如果还有其他运行时处理的需求,可以使用 RetentionPolicy.RUNTIME

3. compiler 模块

该模块编译 Annotation Processor 代码,并将其打包成 jar 文件供其他模块使用,一般都起名为 compiler 或 processor。

创建模块

上文说过只要将 jar 放置在了 classpath 中,javac 就会自动查找并执行处理代码。因此只需要创建一个 java library 模块:

build.gradle

apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
    implementation 'com.squareup:javapoet:1.10.0' // 使用 javapoet 生成 .java 文件
    implementation project(':annotation') // 依赖 annotation 模块方便引用其中的注解
    compileOnly 'com.google.auto.service:auto-service:1.0-rc5'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5'
}

最后两行的依赖是谷歌的 auto service。为啥两行后面是一样的?

谷歌 auto service 和 META-INF

谷歌的 auto service 也是一种 Annotation Processor,它能自动生成 META-INF 目录以及相关文件,避免手工创建该文件,手工创建有可能失误(我就写错过路径)。使用 auto service 中的 @AutoService(Processor.class) 注解修饰 Annotation Processor 类就可以在编译过程中自动生成文件。

可见 auto service 是给 Annotation Processor 服务的 Annotation Processor,是不是很有趣。可能有人要问 auto service 的 META-INF 目录和文件怎么办?不是还可以手工写嘛。

手工怎么写这个 META-INF? 在 jar 包中路径是
META-INF/service/javax.annotation.processing.Processor,见下图:

但在项目中应该放在哪里呢?见下图:

手工生成这个 META-INF 目录和文件就不用引入 auto service 依赖了。

如果引入的话,还要注意有两个配置 compileOnlyannotationProcessor

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

这两个重复写的原因就是 auto service 并没有将 annotation 和 processor 分割成两个项目,而是混到了一起。

  • compileOnly 所需的只是 @AutoService 这个注解。表示只参与编译过程并不打包到最终产物 jar 文件中,而注解 @AutoService 的 RetentionPolicy 就是 SOURCE,不会编译生成到 class 文件中。即便使用 compile 依赖,最终生成的 jar 包中只会多了 auto service 提供的注解,其他 class 文件部分不受影响,因此可以使用 compileOnly 只参与编译过程。
  • annotationProcessor 这个是新版 gradle 提供的 java plugin 内置的配置项,代替了早期第三方提供的 android-apt 插件。而且,敲重点,在 gradle 5.+ 中将 Annotation Processor 从编译期 classpath 中去除了,javac 也就无法发现 Annotation Processor。此处如果按照 gradle 4.+ 的写法,只写一个 compileOnly 是无法使用 auto service 的 Annotation Processor 的。必须要使用 annotationProcessor 来配置 Annotation Processor 使其生效。

定制化 Processor

两个部分:

  1. 定义文件生成规则
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // 因为运行在 javac 执行过程中,打印必须使用 messager,而不能使用 System.out
    messager.printMessage(Diagnostic.Kind.NOTE, "processing");
    // 此处正经工具应该使用参数 set 和 roundEnvironment 根据注解的具体使用情况来生成代码
    // 本文主要讲步骤和配置,不涉及这个部分。仅生成了一个独立的 java 文件。
    MethodSpec main = MethodSpec.methodBuilder("main")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .returns(void.class)
            .addParameter(String[].class, "args")
            .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
            .build();
    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addMethod(main)
            .build();
    JavaFile javaFile = JavaFile.builder("com.example.study.app", helloWorld)
            .build();
    try {
        // 最后要将内容写入到 java 文件中,这里必须使用 processingEnv 中获取的 Filer 对象
        // 它会自动处理路径问题,我们只需要定义好包名类名和文件内容即可。
        Filer filer = processingEnv.getFiler();
        javaFile.writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 返回值表示处理了 set 参数中包含的所有注解,不会再将这些注解移交给编译流程中的
    // 其他 Annotation Processor。一般都不会有多个 Annotation Processor,一般都写 true。
    return true;
}
  1. 配置 Processor。有两种配置方法:可以用注解的方式也可以重写 AbstractProcessor 的某些方法。如果两种方式都定义,则会使用方法的版本,但从工程的角度应该只用一种方法来设置,以免混淆。这些注解设置为了 RetentionPolicy.RUNTIME 类型,如果不重写相关方法,AbstractProcessor 中方法的默认实现则会使用反射来获取注解中设置的值。
// 注解方式
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.study.annotation.NiceField")
public class MyProcessor extends AbstractProcessor {
    // 重写方法方式
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(NiceField.class.getCanonicalName());
    }
}
  • SupportedSourceVersion 指定最低支持的源代码版本,这是指 javac 的版本,对安卓来说只有 7 和 8 的区别,直接使用当前编译环境最高支持版本即可:SourceVersion.latestSupported();
  • SupportedAnnotationTypes 指定该 Annotation Processor 可以处理哪些注解,这里要返回一个字符串的集合,字符串内容是这些注解的完整类路径,即 class.getCanonicalName()

4. app 模块

该模块用来测试和验证 Annotation Processor,是一个简单的 android application 模块。

创建模块

就不贴图了,默认一个 android 项目就会有一个 app 模块,看下面的 build.gradle:

// 前半部分都是自动生成的不用细看
apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.study.app"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
// 上面部分都是自动生成的不用细看
dependencies {
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    // 使用 annotationProcessor 来指定作为 Annotation Processor 的模块
    annotationProcessor project(':compiler')
    // 引入自定义的注解
    implementation project(':annotation')
}

使用注解

package com.ajeyone.study.aptda;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.ajeyone.study.annotation.NiceField;

public class MainActivity extends AppCompatActivity {
    @NiceField //  在这里
    int currentValue;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        currentValue = currentValue + 1;
    }
}

构建后生成了 HelloWorld.java:

总结

本文介绍了 APT 的原理以及自定义 APT 的流程和步骤。在研究过程中也发现了一些坑:

  • gradle 脚本中要使用 annotationProcessor 来指定 Annotation Processor 库。
  • 谷歌 auto service 没有分离 annotation 和 processor,在新版的 gradle 脚本中需要用 annotationProcessor 将其指定为 Annotation Processor 才会起作用。

关于 APT 中另一个块重要的内容,如何通过注解获取源代码的信息,并生成目标源文件,建议阅读一些流行开源项目的源代码,比如 ButterKnife,Dagger2 等等。

另外还有一个比较新的东西是 incremental annotation processor,这个是 gradle 4.7 就搞出来的加快编译速度的功能,跟 java 本身没有关系,但是要引入 gradle 提供的一些工具来修改 Annotation Processor,感兴趣的可以参考一下官网的资料以及一些开源项目的实现,比如 Dagger2。https://docs.gradle.org/4.7/userguide/java_plugin.html#sec:incremental_annotation_processing

参考资料

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

推荐阅读更多精彩内容