Cavern.sigma
Welcome to Cavern.sigma
最近試著用了一下 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: --:-- 
現在還沒有留言!