Annotation Processing 顧名思義就是在 compile 的時候 process 我們的 annotation,然後可以依據這些資訊自動產生一些規律性的程式碼,塞回我們的 source code(但通常是被放在 build
folder 下,所以一般不會意識到)。
如果你還沒看過上一篇我們介紹的 Annotation,建議你花個幾分鐘看一下會更有概念喔!
[Android 十全大補] Annotation
Annotation Processing 屬於偏進階讀者的內容,如果你剛入門(坑)沒多久,可以看完整個系列文章再回頭看這一篇喔。
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:
build
folder 下,通常依賴我們的 annotation module。Annotation module 跟 Library 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 真好。
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()
}
ProcessingEnvironment
拿到 filer 跟 messager 來做寫檔案跟印 log 的事情喔。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
SourceVersion.latestSupported()
即可。寫檔案最簡單的方式就是不斷的操作 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