Android - 沒有比這更新鮮的注解教程了 AS3.4 Gradle5

Pixabay License

網(wǎng)上有很多 APT 相關(guān)教程,最近開始學(xué)這個(gè),發(fā)現(xiàn)有一些內(nèi)容已經(jīng)過時(shí)了,在使用過程中也發(fā)現(xiàn)了一些坑,總結(jié)一下,形成這篇教程。

本文開發(fā)環(huán)境:2019年5月初最新版本的 Android Studio 3.4、Android Plugin 3.4.0、Gradle 5.1.1。

本教程需要讀者了解注解 Annotation 的基本知識(shí),不涉及 Annotation 運(yùn)行時(shí)反射的用法,專注于自定義 APT 的流程和步驟,以及使用新版 AS 和 gradle 的注意事項(xiàng)。

簡(jiǎn)介

APT,Annotation Processing Tool,注解處理工具,是 JDK 提供的一個(gè)工具。注意是 Java 語言支持的,不是安卓特有的東西,這點(diǎn)對(duì)于理解 APT 有一些作用。早期的 JDK 提供了一個(gè)單獨(dú)的 apt 程序,后來被整合到 javac 中了。它最常見的用法就是根據(jù)注解自動(dòng)生成源代碼,很多流行的庫都使用了注解處理器來生成代碼,比如 ButterKnife 會(huì)生成資源與變量綁定的代碼,讓開發(fā)者不用手寫繁瑣重復(fù)的 findViewById。

原理

那么 javac 是怎么使用 APT 生成代碼的呢?javac 并不知道你想怎么生成代碼,需要你按照 javac 提供的規(guī)則和接口來自定義 Annotation Processor。注意這是 Java 語言定義的規(guī)則。

接口

javax.annotation.processing.AbstractProcessor,實(shí)現(xiàn)這個(gè)抽象類,在 process() 方法中自定義生成代碼的細(xì)節(jié)??梢苑Q它為注解處理器 Annotation Processor。只有這一個(gè)類型作為接口,當(dāng)然類中還有一些其他方法用來設(shè)置 Annotation Processor 的屬性。

規(guī)則

  • 一個(gè) Annotation Processor 想要參與到 javac 的編譯過程中,就要被編譯打包成一個(gè) jar 文件。
  • 這個(gè) jar 文件要放置在編譯期的 classpath 中,javac 會(huì)自動(dòng)查找 classpath 中所有的 Annotation Processor,自動(dòng)完成注解處理。然而新版的 gradle 5 不再將 Annotation Processor 放在編譯期的 classpath 中,導(dǎo)致還需要額外處理,這個(gè)是 gradle 的行為,處理方法見下文。
  • 這個(gè) jar 文件中包含一個(gè) META-INF/service/javax.annotation.processing.Processor 文件,文件內(nèi)容是文本,每行一個(gè) Annotation Processor 的完整類名稱。

安卓和 gradle

以上是 Java 的基礎(chǔ)規(guī)則,到了 gradle 中就要按照 Java Plugin 的語法和規(guī)則來配置。gradle 一直致力于提高編譯速度,在新版的 gradle 5.+ 中,為了推行更快速的增量編譯,關(guān)閉了一個(gè)默認(rèn)功能,導(dǎo)致由 gradle 4.+ 升級(jí)上來的項(xiàng)目有可能構(gòu)建失敗,這其中的彎彎繞繞和坑坑洼洼在下面的步驟中詳細(xì)講解。

步驟

1. 項(xiàng)目架構(gòu)

分 3 個(gè)模塊:

  • annotation 模塊:用來定義注解。
  • compiler 模塊:用來定義 Annotation Processor。
  • app 模塊:使用注解的應(yīng)用模塊。

為什么要分這么多模塊?其中 app 模塊是用來測(cè)試的,測(cè)試新定義的 Annotation Processor 能否成功運(yùn)行。annotation + compiler 如果寫得糙一點(diǎn)可以合并在一起,例如谷歌的 auto service。但兩者的目的并不一樣,annotation 是專門定義注解的,而 compiler 是處理注解的。最重要的是,annotation(RetentionPolicy.CLASS,RetentionPolicy.RUNTIME)是需要被編譯到 app 項(xiàng)目的 class 文件中的,而 compiler 沒有必要進(jìn)入 app 中。

2. annotation 模塊

該模塊是定義注解用的,可以包含多個(gè)注解,例如 ButterKnife 就有二十多個(gè)注解定義。而且只有注解的定義,沒有其他任何代碼。這樣做的原因主要是在架構(gòu)上能單獨(dú)隔離一個(gè)完整內(nèi)聚的功能,可以被其他模塊引用,比如 app 模塊必須引用,compiler 模塊可以引用。

創(chuàng)建模塊

annotation 模塊中只有注解定義,可以直接定義為 java library,而不用定義為 android library,定義為安卓庫反而會(huì)限制它被其他 java library 引用。

build.gradle

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

然后就可以在這個(gè)模塊中添加注解定義了。

注解簡(jiǎn)介

定義注解使用 @interface 關(guān)鍵字,然后使用元注解 @Target@Retention 定義注解的修飾目標(biāo)和保留策略:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface NiceField {}
  • @Target 指定和約束注解能修飾的代碼元素,可以是類、字段、方法、參數(shù)等等。
  • @Retention 指定保留策略,指的是被修飾的代碼,在編譯后是否仍保留注解的策略,有三種:
    • RetentionPolicy.SOURCE:不做任何保留,編譯之后就拋棄。
    • RetentionPolicy.CLASS:保留在 class 文件中,但運(yùn)行時(shí)無法使用。
    • RetentionPolicy.RUNTIME:保留在 class 文件中,運(yùn)行時(shí)可以通過反射使用。

當(dāng)注解被 Annotation Processor 處理的時(shí)候,其實(shí)這三種策略的注解都是可以處理的。比如谷歌的 auto service 中的 @AutoService 就是 RetentionPolicy.SOURCE 類型的,用完即拋。如果還有其他運(yùn)行時(shí)處理的需求,可以使用 RetentionPolicy.RUNTIME。

3. compiler 模塊

該模塊編譯 Annotation Processor 代碼,并將其打包成 jar 文件供其他模塊使用,一般都起名為 compiler 或 processor。

創(chuàng)建模塊

上文說過只要將 jar 放置在了 classpath 中,javac 就會(huì)自動(dòng)查找并執(zhí)行處理代碼。因此只需要?jiǎng)?chuàng)建一個(gè) 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,它能自動(dòng)生成 META-INF 目錄以及相關(guān)文件,避免手工創(chuàng)建該文件,手工創(chuàng)建有可能失誤(我就寫錯(cuò)過路徑)。使用 auto service 中的 @AutoService(Processor.class) 注解修飾 Annotation Processor 類就可以在編譯過程中自動(dòng)生成文件。

可見 auto service 是給 Annotation Processor 服務(wù)的 Annotation Processor,是不是很有趣??赡苡腥艘獑?auto service 的 META-INF 目錄和文件怎么辦?不是還可以手工寫嘛。

手工怎么寫這個(gè) META-INF? 在 jar 包中路徑是
META-INF/service/javax.annotation.processing.Processor,見下圖:

但在項(xiàng)目中應(yīng)該放在哪里呢?見下圖:

手工生成這個(gè) META-INF 目錄和文件就不用引入 auto service 依賴了。

如果引入的話,還要注意有兩個(gè)配置 compileOnlyannotationProcessor。

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

這兩個(gè)重復(fù)寫的原因就是 auto service 并沒有將 annotation 和 processor 分割成兩個(gè)項(xiàng)目,而是混到了一起。

  • compileOnly 所需的只是 @AutoService 這個(gè)注解。表示只參與編譯過程并不打包到最終產(chǎn)物 jar 文件中,而注解 @AutoService 的 RetentionPolicy 就是 SOURCE,不會(huì)編譯生成到 class 文件中。即便使用 compile 依賴,最終生成的 jar 包中只會(huì)多了 auto service 提供的注解,其他 class 文件部分不受影響,因此可以使用 compileOnly 只參與編譯過程。
  • annotationProcessor 這個(gè)是新版 gradle 提供的 java plugin 內(nèi)置的配置項(xiàng),代替了早期第三方提供的 android-apt 插件。而且,敲重點(diǎn),在 gradle 5.+ 中將 Annotation Processor 從編譯期 classpath 中去除了,javac 也就無法發(fā)現(xiàn) Annotation Processor。此處如果按照 gradle 4.+ 的寫法,只寫一個(gè) compileOnly 是無法使用 auto service 的 Annotation Processor 的。必須要使用 annotationProcessor 來配置 Annotation Processor 使其生效。

定制化 Processor

兩個(gè)部分:

  1. 定義文件生成規(guī)則
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // 因?yàn)檫\(yùn)行在 javac 執(zhí)行過程中,打印必須使用 messager,而不能使用 System.out
    messager.printMessage(Diagnostic.Kind.NOTE, "processing");
    // 此處正經(jīng)工具應(yīng)該使用參數(shù) set 和 roundEnvironment 根據(jù)注解的具體使用情況來生成代碼
    // 本文主要講步驟和配置,不涉及這個(gè)部分。僅生成了一個(gè)獨(dú)立的 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 {
        // 最后要將內(nèi)容寫入到 java 文件中,這里必須使用 processingEnv 中獲取的 Filer 對(duì)象
        // 它會(huì)自動(dòng)處理路徑問題,我們只需要定義好包名類名和文件內(nèi)容即可。
        Filer filer = processingEnv.getFiler();
        javaFile.writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 返回值表示處理了 set 參數(shù)中包含的所有注解,不會(huì)再將這些注解移交給編譯流程中的
    // 其他 Annotation Processor。一般都不會(huì)有多個(gè) Annotation Processor,一般都寫 true。
    return true;
}
  1. 配置 Processor。有兩種配置方法:可以用注解的方式也可以重寫 AbstractProcessor 的某些方法。如果兩種方式都定義,則會(huì)使用方法的版本,但從工程的角度應(yīng)該只用一種方法來設(shè)置,以免混淆。這些注解設(shè)置為了 RetentionPolicy.RUNTIME 類型,如果不重寫相關(guān)方法,AbstractProcessor 中方法的默認(rèn)實(shí)現(xiàn)則會(huì)使用反射來獲取注解中設(shè)置的值。
// 注解方式
@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 的版本,對(duì)安卓來說只有 7 和 8 的區(qū)別,直接使用當(dāng)前編譯環(huán)境最高支持版本即可:SourceVersion.latestSupported();
  • SupportedAnnotationTypes 指定該 Annotation Processor 可以處理哪些注解,這里要返回一個(gè)字符串的集合,字符串內(nèi)容是這些注解的完整類路徑,即 class.getCanonicalName()。

4. app 模塊

該模塊用來測(cè)試和驗(yàn)證 Annotation Processor,是一個(gè)簡(jiǎn)單的 android application 模塊。

創(chuàng)建模塊

就不貼圖了,默認(rèn)一個(gè) android 項(xiàng)目就會(huì)有一個(gè) app 模塊,看下面的 build.gradle:

// 前半部分都是自動(dòng)生成的不用細(xì)看
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'
        }
    }
}
// 上面部分都是自動(dòng)生成的不用細(xì)看
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;
    }
}

構(gòu)建后生成了 HelloWorld.java:

總結(jié)

本文介紹了 APT 的原理以及自定義 APT 的流程和步驟。在研究過程中也發(fā)現(xiàn)了一些坑:

  • gradle 腳本中要使用 annotationProcessor 來指定 Annotation Processor 庫。
  • 谷歌 auto service 沒有分離 annotation 和 processor,在新版的 gradle 腳本中需要用 annotationProcessor 將其指定為 Annotation Processor 才會(huì)起作用。

關(guān)于 APT 中另一個(gè)塊重要的內(nèi)容,如何通過注解獲取源代碼的信息,并生成目標(biāo)源文件,建議閱讀一些流行開源項(xiàng)目的源代碼,比如 ButterKnife,Dagger2 等等。

另外還有一個(gè)比較新的東西是 incremental annotation processor,這個(gè)是 gradle 4.7 就搞出來的加快編譯速度的功能,跟 java 本身沒有關(guān)系,但是要引入 gradle 提供的一些工具來修改 Annotation Processor,感興趣的可以參考一下官網(wǎng)的資料以及一些開源項(xiàng)目的實(shí)現(xiàn),比如 Dagger2。https://docs.gradle.org/4.7/userguide/java_plugin.html#sec:incremental_annotation_processing

參考資料

  1. http://blog.chengyunfeng.com/?p=1021
  2. http://m.itdecent.cn/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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容