iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 14
0
Mobile Development

Android 十全大補系列 第 14

[Android 十全大補] Annotation Processing

Annotation Processing 顧名思義就是在 compile 的時候 process 我們的 annotation,然後可以依據這些資訊自動產生一些規律性的程式碼,塞回我們的 source code(但通常是被放在 build folder 下,所以一般不會意識到)。

如果你還沒看過上一篇我們介紹的 Annotation,建議你花個幾分鐘看一下會更有概念喔!
[Android 十全大補] Annotation

Annotation Processing 屬於偏進階讀者的內容,如果你剛入門(坑)沒多久,可以看完整個系列文章再回頭看這一篇喔。

Structure

Annotation Processing 因為是在 compile 階段處理 annotation ,所以我們大概可以猜到他跟我們一般寫的程式一定有些設定上的不同,
我們可以從 dagger 的使用方式來略窥一斑:

dependencies {
  implementation 'com.google.dagger:dagger:2.21'
  annotationProcessor 'com.google.dagger:dagger-compiler:2.21'
}

如果我們今天要使用 dagger ,照官網的說明必須要引用二個不同的 dependencies,第一個就像我們一直以來使用的 library,而第二個 annotationProcessor(如果是 kotlin 專案的話或許是 kapt )這個關鍵字,就是讓我們的程式碼可以在 compile 階段跑的關鍵喔。

一般來說一個 Annotation Processing 可能會有以下三種 module:

  • Annotation module
    所有共用的 annotation 集合。
  • Library module
    一般基本的 java/kotlin library,如果我們有任何 class/function 需要被加入到整個 project 裡的時候需要這一層,通常依賴我們的 annotation module。
  • Compiler module
    run 在 compile 階段,可以指定我們所關注的 annotation 而獲取相關程式碼物件,進一步做處理並可以輸出 class 檔案到 build folder 下,通常依賴我們的 annotation module。

Annotation module 跟 Library module 相信大家都很熟悉,我們來介紹一下 Compiler module 會有什麼不同的地方!!

Compiler module

我們透過 annotationProcessor 跟系統說我們這個 module 需要跑在 compile 階段,但系統還是不會知道要呼叫我們哪個檔案呀!

就像 Android 一樣所有的畫面入口點必須繼承自 Activity,透過這個標準化的流程,系統可以不要在意每個 class 裡的不同實作,而可以把每個不同的 class 當成一樣的物件。在 annotation processing 的世界這個 class 就是 Processor

public interface Processor {
    Set<String> getSupportedOptions();

    Set<String> getSupportedAnnotationTypes();

    SourceVersion getSupportedSourceVersion();

    void init(ProcessingEnvironment var1);

    boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);

    Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4);
}

就像 Android 雖然有 Activity 這個共同介面,但我們實際使用的通常是 AppCompatActivity,Processor 也是一樣的,我們可以直接用 AbstractProcessor 來幫我們省去一些不重要的事情。

所以我們知道要寫一些 Processor 在我們的 compiler module 裡,但系統怎麼知道去哪裡找呢?全部搜尋的話好像不太有效率,就像 Android 會有一份 AndroidManifest.xml 來描述所有的 components,我們也需要一個地方來告訴系統去哪裡找我們該呼叫的程式碼。

有沒有覺得學了 Android 真好。

AutoService

java 有提供一套標準流程來註冊哪些 class 會被呼叫,但 google 有一套工具可以讓我們透過簡單的 annotation,省去這些瑣碎的流程。(聽起來是不是就是 annotation processing 做的事情呢?)
加入 dependencies 如下:

dependencies {
  implementation 'com.google.auto.service:auto-service:1.0-rc4'
}

之後直接加上 @AutoService(Processor::class) 在我們的實體 Processor class 上就可以囉!
範例如下:

@AutoService(Processor::class)
class XXXXProcessor : AbstractProcessor() {
  //......
}

我們來更仔細看一下 AbstractProcessor 會需要做些什麼事情:

@AutoService(Processor::class)
class MyProcessor : AbstractProcessor() {
    private lateinit var filer: Filer
    private lateinit var messager: Messager

    @Synchronized
    override fun init(processingEnv: ProcessingEnvironment) {
        super.init(processingEnv)
        filer = processingEnv.filer
        messager = processingEnv.messager
    }

    override fun process(set: Set<TypeElement>, roundEnvironment: RoundEnvironment): Boolean {
        // process your annotation along with the code elements
        return true
    }

    override fun getSupportedAnnotationTypes() = setOf(
        // put your annotations here so system will call process with these annotation objects.
    )

    override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
}
  • init:
    這邊我們可以透過 ProcessingEnvironment 拿到 filer 跟 messager 來做寫檔案跟印 log 的事情喔。
  • process:
    整個 annotation processing 實際運作的地方,可以使用 RoundEnvironment 來拿到我們關注的程式碼物件,RoundEnvironment 的定義如下:
public interface RoundEnvironment {
    boolean processingOver();

    boolean errorRaised();

    Set<? extends Element> getRootElements();

    Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);

    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);
}

Element 就是我們程式碼物件的基本型態,
有以下這些常見的型態,例如: PackageElement 代表著 package 物件,TypeElement 就是我們的 class 或是 interface 物件,ExecutableElement 就是我們的 method 或是 constructor 等。
更多資訊請參考:
https://docs.oracle.com/javase/8/docs/api/javax/lang/model/element/Element.html

  • getSupportedAnnotationTypes:
    我們會需要在這個 function 指名說我們到底關注哪些 annotations,系統就會把有這些 annotation 的程式碼丟到我們的 process function 囉。
  • getSupportedSourceVersion:
    通常就是固定寫 SourceVersion.latestSupported() 即可。

JavaPoet/KotlinPoet

寫檔案最簡單的方式就是不斷的操作 string ,當然你也可以選擇更優雅的方式,我們要介紹 square 另二個有名的 open source project:

JavaPoet: https://github.com/square/javapoet
KotlinPoet: https://github.com/square/kotlinpoet

二者都是把程式碼的符號變成物件,所以我們可以比較易讀、好維護的方式來用程式碼建立程式碼,一個簡單的 JavaPoet 範例如下:

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.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

如上所示,我們宣告了一個叫做 main 的 function,然後一個 class 叫做 HelloWorld 包含著這個 function ,結果就會如下:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

如果把 System.out 改成 filer,我們自動產生的程式碼就會建立在 build folder 下囉!

如果你需要個 sample code,這裡提供一個依據 annotation 來建立 Factory class 的小專案:
https://github.com/Jintin/AutoFactory

以上就是今天的內容囉,有任何問題的話歡迎留言。

Android 十全大補已經正式出書上架囉!
有興趣的讀者歡迎參考:
https://www.tenlong.com.tw/products/9789864345786


上一篇
[Android 十全大補] Annotation
下一篇
[Android 十全大補] Room
系列文
Android 十全大補30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言