- はじめに
- プロジェクトの作成
- アノテーションプロセッサの設定
- アノテーションの作成
- プロセッサの作成
- 要素からアクセサーを取得する
- アクセサーのtoString()を行うコードを生成
- クラス生成
- テストの実行
- まとめ
はじめに
以下では、Java アノテーションプロセッサの使い方の基本事項について説明しました。
今回は、アノテーションプロセッサを使った簡単なサンプルを用いて説明します。
アノテーションを付与した Bean に対して、その内容を toString()
するクラスを自動生成することを考えます。
実運用上でメリットがあるものではありませんが、アノテーションプロセッサによるクラス生成のサンプルとして考えてください。
なお、ビルドには Gradle を使うものとして進めます。
プロジェクトの作成
Gradle でプロジェクトを作成し、build.gradle.kkts
を以下のようにします(ここではライブラリプロジェクトとして進めます)。
plugins { `java-library` } repositories { mavenCentral() } dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.7.2") testAnnotationProcessor(project(":lib", "archives")) } tasks.named<Test>("test") { useJUnitPlatform() } tasks.withType<JavaCompile> { options.encoding = Charsets.UTF_8.name() } java { toolchain { languageVersion.set(JavaLanguageVersion.of(18)) } }
アノテーションプロセッサ用のプロジェクトは、マルチモジュールプロジェクトとすることが多いですが、以下のようにテストの依存にアーカイブを指定することで、シングルモジュールプロジェクトでアノテーションのテストが行えます。
testAnnotationProcessor(project(":lib", "archives"))
その他の定義は、特に何でも良いです。
アノテーションプロセッサの設定
アノテーションプロセッサは、サービスローダで動かすので、以下のファイルを用意し、
META-INF/services/javax.annotation.processing.Processor
ファイルの中身は以下のようにします。
pap.MyProcessor
今回は MyProcessor
という名前でアノテーションプロセッサを作成しますが、好きな名前で構いません。
Gradle の増分コンパイルで扱うために、以下のファイルも用意しておきます。
META-INF/services/incremental.annotation.processors
ファイルの中身は以下のようにします。
pap.MyProcessor,isolating
詳細は以下のマニュアルを参照してください。
Incremental annotation processing
アノテーションの作成
ここでは単なるマーカーアノテーションとして以下を作成します。
package pap; import java.lang.annotation.*; @Documented @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { }
このアノテーションはテストパッケージに以下のクラスを用意してアノテーションを付与して置きましょう。
package pap; @MyAnnotation public class Person { public final FullName fullName; public final int age; public Person(FullName fullName, int age) { this.fullName = fullName; this.age = age; } public FullName getFullName() { return fullName; } public int getAge() { return age; } }
package pap; public class FullName { public final String givenName; public final String familyName; public FullName(String givenName, String familyName) { this.givenName = givenName; this.familyName = familyName; } public String getGivenName() { return givenName; } public String getFamilyName() { return familyName; } }
@MyAnnotation
を付与した Person
クラス用の toString を行うクラスを自動生成することになります。
プロセッサの作成
プロセッサは AbstractProcessor
を継承したクラスとして以下のように作成します。
@SupportedAnnotationTypes("pap.MyAnnotation") public class MyProcessor extends AbstractProcessor { @Override public void init(ProcessingEnvironment env) { super.init(env); env.getMessager().printMessage(Diagnostic.Kind.NOTE, "init MyProcessor."); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { try { for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) { TypeElement type = (TypeElement) element; // ... } } catch (Exception e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage()); } return false; } }
@SupportedAnnotationTypes
で先ほど作成したアノテーションを指定しています。
process()
メソッド内では、MyAnnotation
が付与された要素についてループして処理を行います(実際の処理はまだありません)。
要素からアクセサーを取得する
Element からアクセサーを取得するメソッドを用意します。
private List<ExecutableElement> accessor(TypeElement element) { return element.getEnclosedElements().stream() .filter(e -> e.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast) .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) .filter(e -> e.getReturnType().getKind() != TypeKind.VOID) .filter(e -> e.getParameters().size() == 0) .filter(e -> e.getSimpleName().toString().startsWith("get") || e.getSimpleName().toString().startsWith("is")) .toList(); }
getEnclosedElements()
で当該クラスの要素が取得できるため、getXxx
や isXxx
といったメソッドを抽出しています。
抽出したアクセサーのプロパティ名を抽出するメソッドも用意しておきます。
private String name(ExecutableElement accessor) { var name = accessor.getSimpleName().toString(); if (name.startsWith("get")) { return name.substring(3, 4).toLowerCase() + name.substring(4); } else if (name.startsWith("is")) { return name.substring(2, 3).toLowerCase() + name.substring(3); } else { return name; } }
get()
というメソッドがあった場合など、いろいろと考慮していないです。あくまでサンプルです。
アクセサーのtoString()を行うコードを生成
アクセサーの種類に応じて、toString() を行うコードを作成するコードです。
private String build(String parentPath, Element element) { if (!element.getKind().isClass()) { return ""; } TypeElement typeElement = (TypeElement) element; String code = accessor(typeElement).stream().map(accessor -> { TypeMirror returnType = accessor.getReturnType(); if (returnType.getKind().isPrimitive()) { return """ "%s:" + %s.%s()""" .formatted(name(accessor), parentPath, accessor.getSimpleName()); } else if (returnType.toString().startsWith("java.") || returnType.toString().startsWith("javax.")) { return """ "%s:" + %s.%s().toString()""" .formatted(name(accessor), parentPath, accessor.getSimpleName()); } else { return write(parentPath + "." + accessor.getSimpleName() + "()", processingEnv.getTypeUtils().asElement(returnType)).toString(); } }).collect(Collectors.joining(" + \",\" + ")); return """ "%s{" + %s + "}" """ .formatted(typeElement.getSimpleName(), code); }
プリミティブ型やjavaパッケージのものはそのまま toString を行い、それ以外のものは再帰的に処理を行います。
クラス生成
先ほど空白にしておいた process メソッドの中身を実装します。
for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) { TypeElement type = (TypeElement) element; FileObject fo = processingEnv.getFiler().createSourceFile(type.getQualifiedName() + "_"); try (PrintWriter pw = new PrintWriter(fo.openOutputStream())) { pw.println("package " + processingEnv.getElementUtils().getPackageOf(type).getSimpleName() + ";"); pw.println(""); pw.println(""" public class %s { public static String toStr(%s obj) { return %s; } } """.formatted(type.getSimpleName() + "_", type.getSimpleName(), build("obj", element))); } }
processingEnv.getFiler().createSourceFile()
で取得した FileObject へ書き込みを行います。
パッケージ文と、クラス定義を行います(今回の例ではimport文は不要です)。
テストの実行
テストを実行すれば、アノテーションプロセッサにより以下のクラスが生成されます(インデントは手で整形しています)。
package pap; public class Person_ { public static String toStr(Person obj) { return "Person{" + "FullName{" + "givenName:" + obj.getFullName().getGivenName().toString() + "," + "familyName:" + obj.getFullName().getFamilyName().toString() + "}" + "," + "age:" + obj.getAge() + "}"; } }
テストとしては以下のようになります。
class MyProcessorTest { @Test void test() { var parson = new Person(new FullName("Bob", "dylan"), 81); assertEquals("Person{FullName{givenName:Bob,familyName:dylan},age:81}", Person_.toStr(parson)); } }
まとめ
アノテーションプロセッサのコード生成について例を使って説明しました。
プロセッサの全体像は以下のようになりました。
package pap; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; import javax.tools.FileObject; import java.io.PrintWriter; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @SupportedAnnotationTypes("pap.MyAnnotation") public class MyProcessor extends AbstractProcessor { @Override public void init(ProcessingEnvironment env) { super.init(env); env.getMessager().printMessage(Diagnostic.Kind.NOTE, "init MyProcessor."); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { try { for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) { TypeElement type = (TypeElement) element; FileObject fo = processingEnv.getFiler().createSourceFile(type.getQualifiedName() + "_"); try (PrintWriter pw = new PrintWriter(fo.openOutputStream())) { pw.println("package " + processingEnv.getElementUtils().getPackageOf(type).getSimpleName() + ";"); pw.println(""); pw.println(""" public class %s { public static String toStr(%s obj) { return %s; } } """.formatted(type.getSimpleName() + "_", type.getSimpleName(), build("obj", element))); } } } catch (Exception e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage()); e.printStackTrace(); } return false; } private String build(String parentPath, Element element) { if (!element.getKind().isClass()) { return ""; } TypeElement typeElement = (TypeElement) element; String code = accessor(typeElement).stream().map(accessor -> { TypeMirror returnType = accessor.getReturnType(); if (returnType.getKind().isPrimitive()) { return """ "%s:" + %s.%s()""" .formatted(name(accessor), parentPath, accessor.getSimpleName()); } else if (returnType.toString().startsWith("java.") || returnType.toString().startsWith("javax.")) { return """ "%s:" + %s.%s().toString()""" .formatted(name(accessor), parentPath, accessor.getSimpleName()); } else { return write(parentPath + "." + accessor.getSimpleName() + "()", processingEnv.getTypeUtils().asElement(returnType)).toString(); } }).collect(Collectors.joining(" + \",\" + ")); return """ "%s{" + %s + "}" """ .formatted(typeElement.getSimpleName(), code); } private List<ExecutableElement> accessor(TypeElement element) { return element.getEnclosedElements().stream() .filter(e -> e.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast) .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) .filter(e -> e.getReturnType().getKind() != TypeKind.VOID) .filter(e -> e.getParameters().size() == 0) .filter(e -> e.getSimpleName().toString().startsWith("get") || e.getSimpleName().toString().startsWith("is")) .toList(); } private String name(ExecutableElement accessor) { var name = accessor.getSimpleName().toString(); if (name.startsWith("get")) { return name.substring(3, 4).toLowerCase() + name.substring(4); } else if (name.startsWith("is")) { return name.substring(2, 3).toLowerCase() + name.substring(3); } else { return name; } } }
アノテーションを使う機会は少ないかも知れませんが、取っ掛かりとして良い日本語の情報源が少ないため、ここにまとめてみました。