Android - 一篇嶄新的 APT 教程 AS3.4 Gradle5

網上有很多 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章