Java - Annotation Processing 紀錄
最近試著用了一下 Java 的 Annotation 跟 Annotation Processor ,在這裡做個紀錄。
# Scenario
## Problem
需要不同的子類來區分不同的東西。但是目的只是像標記那樣,子類裡面不用做別的事。
所以只需要繼承一個特定類,然後提供 constructor 就可以。
但是這樣這些新的子類就幾乎全部 boilerplate code ,他們全都長成這樣:
```java
public class Child extends Parent {
public Child(args...) { super(args...); }
//more constructors
}
```
不知為何,我沒有選擇用 enum 去標記,而是用開新類別的方式。
## Solution
能不能使用一個 annotation 去生成這些簡單的子類別?
去抓取 Parent class 裡面的 constructors 之後,在子類別裡寫出一一對應的 constructors 就好了?
# Solving
要做到這件事,我們需要兩樣東西:Annotation 跟 Annotation processing ,就是「宣告一個 annotation 出來」跟「處理 annotation 兩個步驟」。
(順帶一提, Annnotation 在 Java 裡面就是那個 @ 開頭的東西,例如常常出現在方法上面的 `@Override`)
## Environment
IntelliJ IDEA, Gradle project
## Create Annotation
首先是弄一個 Annotation 出來。
因為我們要做程式生成的關係,生成的這個部分必須先被編譯然後執行。個人是直接在原來的專案下面開了一個新的 gradle module (下面使用 `Annotation` 當作 module 的名字)。
在這個 Annotation 的 module 裡面,新增一個 `SimpleSubclass.java` ,內容如下:
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface SimpleSubclass {
String value();
}
```
`@Target` 標示的是你想要這個 Annotation 可以被標在哪裡,我們想要他可以被標在 Parent class 上,所以選的是 `TYPE`。
`@Retention` 標示的是這個 Annotation 存活時間,我們這裡只是在編譯時能被我們讀到然後新增 class 就行,所以選 `SOURCE` 。
`value()` 是這個 Annotation 要放的參數,下面會看到。我們這裡想要放要生成的 subclass 的名字。
## Using Annotation
這時候就這個 `SimpleSubclass` 的 Annotation 就已經可以使用了。修改原先外圍 module (假定我們是要在原來外面那個 module 使用)的 `build.gradle` :
```gradle
//....
dependencies {
//...
compileOnly project(":Annotation")
//...
}
```
來把我們剛才的 Annotation module 加進來。
Gradle Sync 結束之後你就發現已經可以快樂使用這個 Annotation 了,像是這樣:
```java
@SimpleSubclass("Child")
public Parent {
//...
}
```
這裡 annotation 裡面放的參數 `Child` 對應的就是上面宣告裡面的 `String value();` 。就是說如果我們拿到「這一個」Annotation 物件的話,那麼呼叫他的 `value()` 就會回傳 `Child` 。
當然,標記之後一點用沒有,`Child` 這個 class 不會平白無故生出來。所以接下來要處理 Annotation 並且生成 subclass 。
## Annotation Processor
處理 Annotation 的方式是使用 Annotation Processor 。
在 Annatation module 裡面加入新的 class ,這裡把他叫做 `SimpleSubclassProcessor` 。
```java
@SupportedAnnotationTypes("SimpleSubclass") //should use fully qualified name
public class SimpleStyleProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
}
}
```
繼承 `AbstractProcessor` ,他會幫你做很多事情,我們的處理邏輯寫在 `process` 裡面就行。
`@SupportedAnnotationTypes` 用來標記這個 processor 要處理什麼 annotation ,裡面的參數應該要放帶著 package name 的 annotation class 的名字(fully qualified name ),但是上面沒說這個 package 叫什麼所以這裡就只有寫 annotation 的名字,做的時候要記得加上 package name 。
(不是很確定只用 annotation name 會不會動)
### process()
首先為了生成 class ,我要用一個工具叫 [JavaPoet](https://github.com/square/javapoet) (其實自己純手寫也可以,但是我不想)。
在 Annotation module 的 build.gradle 裡面把他加入 dependencies:
```gradle
//....
dependencies {
//....
implementation 'com.squareup:javapoet:1.13.0' //版本自己改
//....
}
```
接下來就是 `process` 裡面了。
這裡面的內容基本上都是一邊查網路一邊查 API 湊出來的,反正之後再寫肯定還得摸索,這裡就上 comment 說明一下就好。
(本來想放在 gist 上,可是他好像秀逗了 indent 寬度給我調成 8,我還沒辦法弄成 4)
```java
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//annotations 是我們要處理的 annotation types,在這裡基本上就是 SimpleSubclass ,因為我們上面指定了要處理這個。
for (TypeElement annotation: annotations) {
//找到被這個 annotation 標記的東西
Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith(annotation);
for (Element e: annotated) {
if (e.getKind() != ElementKind.CLASS) continue; //確保一下標記的是 class
//取得這個 class 的 package name ,我們想要生成的 subclass 跟被標記的 class 在一樣的 package
String packageName =
processingEnv.getElementUtils()
.getPackageOf(e)
.getQualifiedName()
.toString();
//取得 annotation 本體,class 是關鍵字這裡用 clazz 代替
SimpleSubclass clazz = e.getAnnotation(SimpleSubclass.class);
//從 annotation 身上拿到使用者想要的 subclass 名稱
String name = clazz.value();
//給定名字創造新的 class ,要是 public ,繼承 e 這個 class (e 是被標記的東西,可以回去看上面)
TypeSpec.Builder typeBuilder =
TypeSpec.classBuilder(name)
.addModifiers(Modifier.PUBLIC)
.superclass(e.asType());
//取得 e 的 constructors
List<ExecutableElement> constructors =
e.getEnclosedElements()
.stream()
.filter(element -> element.getKind() == ElementKind.CONSTRUCTOR)
.map(element -> (ExecutableElement) element)
.collect(toList());
for (ExecutableElement cons: constructors) {
//創造要給 subclass 的 constructor
MethodSpec.Builder constructorBuilder =
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC);
List<? extends VariableElement> args = cons.getParameters();
for (VariableElement arg: args) {
constructorBuilder =
constructorBuilder
.addParameter(TypeName.get(arg.asType()), arg.getSimpleName().toString());
}
//這裡就是真正把 subclass 給 build 出來
typeBuilder.addMethod(
constructorBuilder.addStatement(
"super(" + args.stream().map(VariableElement::getSimpleName).collect(Collectors.joining(", ")) + ")"
).build()
);
}
//給定 package 跟 subclass (上面 typeBuilder 一連串設定的東西),弄個檔案出來
JavaFile file =
JavaFile.builder(packageName, typeBuilder.build())
.build();
try {
//寫檔
file.writeTo(processingEnv.getFiler());
} catch (IOException err) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, err.toString());
}
}
}
//表示我們處理過這個 annotation 了
return true;
}
```
另外,這個沒有處理 generic 的情況。
### register processor
接下來要告訴 compiler 編譯的時候來執行這個 processor 。
我們這裡用 AutoService ,因為比較方便。
在 Annatotion module 的 build.gradle 裡面把 AutoService 加進來:
```gradle
//....
dependencies {
//....
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
compileOnly 'com.google.auto.service:auto-service:1.0.1'
//....
}
```
然後在 `SimpleStyleProcessor` 加上這個 annotation 變成:
```java
@SupportedAnnotationTypes("SimpleSubclass") //should use fully qualified name
@AutoService(Processor.class)
public class SimpleStyleProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//....
}
}
```
### Using processor
最後一步就是在原來的 module 裡面使用 processor ,在使用 annotation 的 module 裡面的 build.gradle 把 annotation processor 加進來:
```gradle
//....
dependencies {
//....
annotationProcessor project(":Annotation")
//....
}
```
然後 build 外面使用 annotation 的 module,沒有意外的話你會看到 `Child` 這個 class 在 `build/generated/sources/annotationProcessor/java/main` 裡面。好耶!
而且在產生之後你就可以開始使用這個 class 了。好耶!
2022-08-20 22:23:17
留言
Last fetch: --:--
現在還沒有留言!