- はじめに
- Pluggable Annotation Processing
- アノテーションプロセッサ
- アノテーションプロセッサの実行
- ProcessingEnvironment と RoundEnvironment
- Element と TypeMirror
- Element
- TypeMirror
- AnnotationMirror
- Elements と Types
- Filer
- まとめ
はじめに
Java6 から有る Pluggable Annotation Processing 仕様(Java5 における Annotation Processor Tool の後継)ですが、近年のビルドタイム指向に伴い、ランタイムでリフレクションに頼るのではなく、ビルドタイムでコード生成することで初期化の短縮を行う用途での利用が増えているように思います。
このような背景も踏まえ、本稿では、今更ながらのアノテーションプロセッサの基本について説明します。
実例から入った方がわかりやすい方は、以下からどうぞ。
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(); }
アノテーション処理の過程では、Element
と TypeMirror
から取得した情報を使い、Filer
でクラスを生成することになります。
Element
と TypeMirror
の違いについて混乱するハズなので、以下に説明します。
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
を取得できます。
Elements
は Element
に対するユーティリティ・メソッドを提供します。
以下のようなメソッドが色々あります.
public interface Elements { TypeElement getTypeElement(CharSequence canonicalName); List<? extends Element> getAllMembers(TypeElement type); String getDocComment(Element e); ... }
ProcessingEnvironment
から getTypeUtils()
で Types
を取得できます。
Types
は TypeMirror
に対するユーティリティ・メソッドを提供します。
以下のようなメソッドが色々あります.
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 アノテーションプロセッサの使い方として、基礎となる前提知識を説明しました。
次回は、具体的なアノテーションプロセッサの作り方について説明します。