Java アノテーションプロセッサの使い方(基礎編)


はじめに

Java6 から有る Pluggable Annotation Processing 仕様(Java5 における Annotation Processor Tool の後継)ですが、近年のビルドタイム指向に伴い、ランタイムでリフレクションに頼るのではなく、ビルドタイムでコード生成することで初期化の短縮を行う用途での利用が増えているように思います。

このような背景も踏まえ、本稿では、今更ながらのアノテーションプロセッサの基本について説明します。


実例から入った方がわかりやすい方は、以下からどうぞ。

blog1.mammb.com


Pluggable Annotation Processing

アノテーションプロセッサは、コンパイル時に、アノテーションに基づいてコードを検証、生成する仕組みです(プリプロセッサ)。 アノテーションを付与したクラスなどを用意すれば、コンパイル中(クラスファイル生成前のAST構築後)にフックすることで、対象に応じた操作を挟みこむことができます。

JPA におけるスタティックメタモデルの生成や、コンパイル時の静的解析の実施などで利用できます。 なお、Lombok のような既存コードの変更は、標準APIではできません(Lombok はASTを無理やり書き換えています)。

JSR は以下になります。

JSR 269: Pluggable Annotation Processing API


アノテーションプロセッサ

アノテーションプロセッサの入り口は、javax.annotation.processing.Processor インターフェースを実装したクラスで用意します。

実際には javax.annotation.processing.AbstractProcessor が用意されているので、こちらを利用することになるでしょう。

@SupportedAnnotationTypes({ "path.to.MyAnnotation" })
public class MyProcessor extends AbstractProcessor {

    @Override
    public void init(ProcessingEnvironment env) {
        super.init(env);
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    @Override
    public boolean process(Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnv) {
        return false;
    }

}

@SupportedAnnotationTypes対象とするアノテーションを指定します("*" とワイルドカード指定することもできます)。 init() でプロセッサの初期化処理を行い、process() に行いたい処理を記載します。

process()通常複数回呼び出されます。これは、アノテーションプロセッサにより生成されたソースを対象に、さらにアノテーションプロセッサで処理をするケースの応じたものです。

process() の引数の RoundEnvironment のラウンドは、最初の呼び出しをラウンド1、次の呼び出しをラウンド2といったラウンド毎の環境を表します(AbstractProcessor は複数回インスタンス化されず、各ラウンドで使いまわされます)。

process() の第一引数の annotations には、処理対象のアノテーションが入っています。


アノテーションプロセッサの実行

アノテーションプロセッサを実行するには、javac の -processor オプションでプロセッサを渡します。

$ javac -processor MyProcessor ・・・

または、サービスローダでアノテーションプロセッサを指定します(通常はこちらを利用します)。

javax.annotation.processing.Processor というファイルを META-INF/services/ 配下に作成し、

META-INF/services/
   javax.annotation.processing.Processor

javax.annotation.processing.Processor にアノテーションプロセッサのFQCNを記載します(複数記載可能です)。

com.example.MyProcessor

jar で固めてクラスパスに含めれば、勝手にアノテーションプロセッサが動きます。

Gradle の場合は以下のように annotationProcessor で依存を定義するだけです。

dependencies {
    annotationProcessor("...")
}


ProcessingEnvironment と RoundEnvironment

プロセッサの init() で受け取る ProcessingEnvironment には以下のメソッドがあります。

public interface ProcessingEnvironment {
    Elements getElementUtils();
    Types getTypeUtils();
    Filer getFiler();
    Messager getMessager();
    SourceVersion getSourceVersion();
    Map<String, String> getOptions();
    Locale getLocale();
}

getElementUtils()getTypeUtils() はそれぞれ Element と TypeMirror に対するユーティリティです。それぞれ以下の通りです。

  • Elements ユーティリティ

    • ProcessingEnvironment.getElementUtils() で取得
    • Element 操作のユーティリティメソッドを提供
  • Types ユーティリティ

    • ProcessingEnvironment.getTypeUtils() で取得
    • TypeMirror 操作のユーティリティメソッドを提供

getFiler() で取得する Filer にてクラス生成を行います(後述します)。

getMessager() で取得する Messager からアノテーションプロセス中のログ出力を行います。


プロセッサの process() で受け取る RoundEnvironment には以下のメソッドがあります。

public interface RoundEnvironment {
    boolean processingOver();
    boolean errorRaised();
    Set<? extends Element> getRootElements();
    Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}

対象のアノテーションが付与された要素を以下のように取得し、当該要素に応じた処理を行います。

for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
   var name = element.getSimpleName().toString();
}

アノテーション処理の過程では、ElementTypeMirror から取得した情報を使い、Filer でクラスを生成することになります。

ElementTypeMirror の違いについて混乱するハズなので、以下に説明します。


Element と TypeMirror

アノテーションプロセッサで処理をする上で最初に混乱するのは、操作対象がクラスではなく、コンパイル途中のASTを対象に操作する必要があることです。

クラスファイル(バイトコード)がまだ作成されていないため、主に以下の2つを対象に扱うことになります。

  • Element モジュール、パッケージ、クラス、メソッドなどのプログラム要素を表す
  • TypeMirror Javaプログラミング言語の型を表す

Element はコード上のプログラム要素で、Element.asType() により、その型定義 TypeMirror にアクセスできます。

例えば、以下の場合、

@MyAnnotation
public String name;

Element はこの name要素 で、element.getSimpleName()name が得られ、element.getModifiers() で public かどうかなどが判断できます。

element.asType() で取得した TypeMirror からは、ここでは String の型情報にアクセスできます。

String クラスの持つメソッドなどを取得したい場合には、String の TypeMirror から Types を使って String クラスの要素を取得して処理する流れになります。

Types typeUtils = ...
Element stringClass = typeUtils.asElement(element.asType());
List<? extends Element> stringsElements = elm.getEnclosedElements();


Element

javax.lang.model.element.Element は、モジュール、パッケージ、クラス、メソッドなどのプログラム要素を表すインターフェースです。

Element は以下のような各プログラム要素の親インターフェースとなっています。

  • PackageElement:パッケージのプログラム要素を表します
  • ModuleElement:モジュール・プログラム要素を表します
  • TypeElement:クラスまたはインタフェースのプログラム要素を表します(enum型はクラスの一種、注釈型はインタフェースの一種)。
  • TypeParameterElement:ジェネリックなクラス、インタフェース、メソッド、またはコンストラクタの要素の仮型パラメータを表します
  • ExecutableElement:クラスまたはインタフェースのメソッド、コンストラクタ、または初期化子(静的またはインスタンス)を表します(注釈型要素を含む)
  • VariableElement:フィールド、enum定数、メソッドまたはコンストラクタのパラメータ、ローカル変数、リソース変数、または例外パラメータを表します


Element オブジェクトから各要素を取得する場合には、instanceof ではなく、getKind() メソッドにより要素を特定するか、visitor を使って処理します。

Element.getKind() により得られる ElementKind は以下があります。

public enum ElementKind {
    PACKAGE,
    MODULE,
    ENUM,
    CLASS,
    ANNOTATION_TYPE,
    INTERFACE,
    ENUM_CONSTANT,
    FIELD,
    PARAMETER,
    LOCAL_VARIABLE,
    EXCEPTION_PARAMETER,
    METHOD,
    CONSTRUCTOR,
    STATIC_INIT,
    INSTANCE_INIT,
    TYPE_PARAMETER,
    OTHER,
    RESOURCE_VARIABLE;
}

これらの種類に応じて、Element を、例えば ElementKind.CLASS の場合は TypeElement にキャストして使うことになります。


TypeMirror

javax.lang.model.type.TypeMirror はJavaプログラミング言語の型を表します。

型には、プリミティブ型、宣言された型(クラスおよびインタフェースの型)、配列型、型変数、およびnull型が含まれます。

ワイルドカード型の引数、実行可能ファイルのシグネチャと戻り値の型、パッケージ、モジュール、キーワードvoidに対応する疑似型も表示されます。

  • ExecutableType:メソッド、コンストラクタ、または初期化子を表します
  • PrimitiveType:プリミティブ型を表します
  • ReferenceType:参照型を表します。クラスとインタフェースの型、配列型、型変数、およびnull型があります
    • DeclaredType:(宣言された型である)クラス型またはインタフェース型を表します(java.util.Set<String>などのパラメータ化された型や、raw型が含まれる)。
    • ArrayType:配列型を表します(多次元配列は、コンポーネントの型も配列型である配列型として表されます)
    • NullType:式 null の型です
    • TypeVariable:型変数を表します。型変数は、型、メソッド、またはコンストラクタの型パラメータによって明示的に宣言することができます。また、ワイルドカード型引数の取得変換によって暗黙的に宣言することもできます
  • WildcardType:ワイルドカード型引数を表します(? extends Number など)
  • IntersectionType:共通部分型を表します(型パラメータ<T extends Number & Runnable>など)
  • UnionType:共用体型を表します(multi catch パラメータの型など)


Element と同様に、オブジェクトから各要素を取得する場合には、instanceof ではなく、getKind() メソッドにより要素を特定するか、visitor を使って処理します。

TypeMirror.getKind() により得られる TypeKind は以下があります。

public enum TypeKind {
    BOOLEAN,
    BYTE,
    SHORT,
    INT,
    LONG,
    CHAR,
    FLOAT,
    DOUBLE,
    VOID,
    NONE,
    NULL,
    ARRAY,
    DECLARED,
    ERROR,
    TYPEVAR,
    WILDCARD,
    PACKAGE,
    MODULE,
    EXECUTABLE,
    OTHER,
    UNION,
    INTERSECTION;
}

こちらも、TypeMirror を、例えば TypeKind.DECLARED の場合は DeclaredType にキャストして使うことになります。

型の比較には、後述の Types ユーティリティを使用します。


AnnotationMirror

要素や型に付与されたアノテーションを知るには、 element.getAnnotationMirrors() などで取得できる AnnotationMirror を使います。

AnnotationMirror は注釈を表し、注釈を比較するには、equalsメソッドを使用します。

以下のメソッドが定義されています。

public interface AnnotationMirror {
    DeclaredType getAnnotationType();
    Map<? extends ExecutableElement, ? extends AnnotationValue> getElementValues();
}


Elements と Types

ProcessingEnvironment から getElementUtils()Elements を取得できます。

ElementsElement に対するユーティリティ・メソッドを提供します。

以下のようなメソッドが色々あります.

public interface Elements {
    TypeElement getTypeElement(CharSequence canonicalName);
    List<? extends Element> getAllMembers(TypeElement type);
    String getDocComment(Element e);
    ...
}


ProcessingEnvironment から getTypeUtils()Types を取得できます。

TypesTypeMirror に対するユーティリティ・メソッドを提供します。

以下のようなメソッドが色々あります.

public interface Types {
    Element asElement(TypeMirror t);
    boolean isSameType(TypeMirror t1, TypeMirror t2);
    boolean isSubtype(TypeMirror t1, TypeMirror t2);
    boolean isAssignable(TypeMirror t1, TypeMirror t2);
    ...
}


例えば、取得した typeMirror が List かどうかを調べるには以下のようにすることができます。

TypeMirror list = elements.getTypeElement("java.util.List").asType();
TypeMirror erasure = types.erasure(typeMirror);
boolean isList = types.isAssignable(erasure, list);

getTypeElement() で TypeElement を取得でき、asType() により TypeMirror を取得することができます。

erasure() では java.util.List<String> のような型引数を消し、java.util.List のような TypeMirror を取得できます。

isAssignable() で型の割当が可能かどうかの検証ができます。

面倒ですが、仕方が無いです。


Filer

ソースファイル(.java)の生成は ProcessingEnvironment.getFiler() で取得する Filer により行います。

典型的には以下のようになります。

try {
    FileObject fo = env.getFiler().createSourceFile("foo.Foo");
    try (PrintWriter pw = new PrintWriter(fo.openOutputStream())) {
        pw.println("""
           package foo;
           class Foo { }
           """);
        pw.flush();    
    }
} catch (Exception e) { ... }


まとめ

Java アノテーションプロセッサの使い方として、基礎となる前提知識を説明しました。

次回は、具体的なアノテーションプロセッサの作り方について説明します。