Javaアノテーション処理とビルダーの作成
1前書き
この記事は、
Javaソースレベルの注釈処理の概要
で、コンパイル時に追加のソースファイルを生成するためのこの手法の使用例を示しています。
2アノテーション処理の応用
ソースレベルの注釈処理は、Java 5で初めて登場しました。コンパイル段階で追加のソースファイルを生成するのに便利なテクニックです。
ソースファイルはJavaファイルである必要はありません。ソースコードの注釈に基づいて、あらゆる種類の説明、メタデータ、ドキュメント、リソース、またはその他の種類のファイルを生成できます。
注釈処理は、多くのユビキタスJavaライブラリで活発に使用されています。たとえば、QueryDSLおよびJPAでメタクラスを生成したり、Lombokライブラリの定型コードでクラスを拡張したりするためです。
注意すべき重要なことは
アノテーション処理APIの制限です – それは既存のファイルを変更するためではなく、新しいファイルを生成するためにのみ使用できます
注目すべき例外はhttps://projectlombok.org/[Lombok]ライブラリです。これは、ブートストラップのメカニズムとしてアノテーション処理を使用して自分自身をコンパイルプロセスに組み込み、いくつかの内部コンパイラAPIを介してASTを変更します。このハッキングなテクニックは、意図された注釈処理の目的とは関係がないため、この記事では説明しません。
3注釈処理API
注釈処理は複数ラウンドで行われます。各ラウンドは、コンパイラがソースファイル内の注釈を検索し、これらの注釈に適した注釈プロセッサを選択することから始まります。各注釈プロセッサは、対応するソースで順番に呼び出されます。
このプロセス中にファイルが生成されると、生成されたファイルを入力として別のラウンドが開始されます。このプロセスは、処理段階で新しいファイルが生成されなくなるまで続きます。
各注釈プロセッサは、対応するソースで順番に呼び出されます。このプロセス中にファイルが生成されると、生成されたファイルを入力として別のラウンドが開始されます。このプロセスは、処理段階で新しいファイルが生成されなくなるまで続きます。
注釈処理APIは、
javax.annotation.processing
パッケージにあります。実装する必要がある主なインターフェースは
Processor
インターフェースです。これは、
AbstractProcessor
クラスの形式で部分的に実装されています。このクラスは、私たちが独自のアノテーションプロセッサを作成するために拡張しようとしているものです。
4プロジェクトの設定
注釈処理の可能性を実証するために、注釈付きクラスのための流暢なオブジェクトビルダーを生成するための簡単なプロセッサを開発します。
プロジェクトを2つのMavenモジュールに分割します。そのうちの1つ、
annotation-processor
モジュールはプロセッサ自体をアノテーションと一緒に含み、別の
annotation-user
モジュールはアノテーション付きクラスを含みます。これはアノテーション処理の典型的なユースケースです。
annotation-processor
モジュールの設定は次のとおりです。 Googleのhttps://github.com/google/auto/tree/master/service[auto-service]ライブラリを使用して、後述するプロセッサメタデータファイルと
maven-compiler-plugin
tunedを生成します。 Java 8ソースコード用。これらの依存関係のバージョンはpropertiesセクションに抽出されます。
autoの最新バージョン-service
libraryとhttps://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.apache.maven.plugins%22%20AND%20a%3A%22maven-compiler-plugin% 22[maven-compiler-plugin]はMaven Centralリポジトリにあります。
<properties>
<auto-service.version>1.0-rc2</auto-service.version>
<maven-compiler-plugin.version>
3.5.1
</maven-compiler-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>${auto-service.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
注釈付きソースを持つ
annotation-user
Mavenモジュールは、依存関係のセクションで注釈プロセッサモジュールへの依存関係を追加することを除いて、特別な調整は必要ありません。
<dependency>
<groupId>com.baeldung</groupId>
<artifactId>annotation-processing</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
5注釈を定義する
annotation-user
モジュールにいくつかのフィールドを持つ単純なPOJOクラスがあるとします。
public class Person {
private int age;
private String name;
//getters and setters ...
}
Person
クラスをよりスムーズにインスタンス化するためのビルダーヘルパークラスを作成したいです。
Person person = new PersonBuilder()
.setAge(25)
.setName("John")
.build();
この
PersonBuilder
クラスは、その構造が
Person
setterメソッドによって完全に定義されているため、世代にとって明らかな選択です。
設定メソッド用の
annotation-processor
モジュールに
@ BuilderProperty
アノテーションを作成しましょう。セッターメソッドに注釈が付けられている各クラスに対して
Builder
クラスを生成することができます。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}
ElementType.METHOD
パラメーターを指定した
@ Target
アノテーションを使用すると、このアノテーションはメソッドにのみ配置できます。
SOURCE
保存方針は、この注釈がソース処理中にのみ使用可能で、実行時には使用できないことを意味します。
@ BuilderProperty
アノテーションでプロパティがアノテーションされた
Person
クラスは以下のようになります。
public class Person {
private int age;
private String name;
@BuilderProperty
public void setAge(int age) {
this.age = age;
}
@BuilderProperty
public void setName(String name) {
this.name = name;
}
//getters ...
}
6.
Processor
を実装する
6.1.
AbstractProcessor
サブクラスの作成
annotation-processor
Mavenモジュール内で
AbstractProcessor
クラスを拡張することから始めます。
まず、このプロセッサが処理できる注釈と、サポートされているソースコードのバージョンを指定します。これは、
Processor
インターフェースのメソッド
getSupportedAnnotationTypes
および
getSupportedSourceVersion
を実装するか、または
@ SupportedAnnotationTypes
および
@ SupportedSourceVersion
アノテーションを使用してクラスにアノテーションを付けることによって実行できます。
@ AutoService
アノテーションは
auto-service
ライブラリの一部であり、以下のセクションで説明されるプロセッサメタデータを生成することを可能にします。
@SupportedAnnotationTypes(
"com.baeldung.annotation.processor.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE__8)
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
return false;
}
}
com.baeldung.annotation
パッケージおよびそのすべてのサブパッケージ内の注釈を処理するための
“ com.baeldung.annotation。
”
のような具体的な注釈クラス名だけでなくワイルドカードも指定できます。さらには
“ ”
すべての注釈を処理します。
実装しなければならない唯一のメソッドは、処理自体を実行する
process
メソッドです。これは、一致する注釈を含むすべてのソースファイルに対してコンパイラによって呼び出されます。
注釈は最初の
Set <?として渡されます。 TypeElement> annotations
引数を拡張し、現在の処理ラウンドに関する情報が
RoundEnviroment roundEnv
引数として渡されます。
注釈プロセッサがすべての渡された注釈を処理した場合、戻り値の
boolean
値は
true
になります。それらを一覧の他の注釈プロセッサに渡したくない場合は、
6.2. 情報収集中
私たちのプロセッサはまだ実際には何もしませんので、コードで埋めましょう。
まず、クラス内にあるすべての注釈型を反復処理する必要があります。この例では、
annotations
セットには、ソースファイル内でこの注釈が複数回出現しても
@ BuilderProperty
注釈に対応する単一の要素があります。
それでも、完全を期すために、
process
メソッドを反復サイクルとして実装する方が得策です。
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements
= roundEnv.getElementsAnnotatedWith(annotation);
//...
}
return true;
}
このコードでは、
RoundEnvironment
インスタンスを使用して、
@ BuilderProperty
アノテーションが付けられたすべての要素を受け取ります。
Person
クラスの場合、これらの要素は
setName
メソッドと
setAge
メソッドに対応しています。
@ BuilderProperty
アノテーションのユーザーが、実際にはセッターではないメソッドに誤ってアノテーションを付ける可能性があります。設定メソッド名は
set
で始まり、メソッドは単一の引数を受け取る必要があります。それでは、小麦を籾殻から分離しましょう。
次のコードでは、
Collectors.partitioningBy()
コレクターを使用して、注釈付きメソッドを2つのコレクション(正しく注釈付きの設定メソッドとその他の誤って注釈付きのメソッド)に分割します。
Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
Collectors.partitioningBy(element ->
((ExecutableType) element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")));
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
ここでは、
Element.asType()
メソッドを使用して、
TypeMirror
クラスのインスタンスを受け取ります。これにより、ソース処理段階にしかいない場合でも、型をイントロスペクトすることができます。
アノテーションが誤っているメソッドについてユーザーに警告する必要があるので、
AbstractProcessor.processingEnv
protectedフィールドからアクセス可能な
Messager
インスタンスを使用しましょう。次の行は、ソース処理段階で、誤って注釈が付けられた要素ごとにエラーを出力します。
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BuilderProperty must be applied to a setXxx method "
+ "with a single argument", element));
もちろん、正しいsettersコレクションが空の場合は、現在のタイプ要素セットの反復を継続する意味はありません。
if (setters.isEmpty()) {
continue;
}
settersコレクションに少なくとも1つの要素がある場合は、それを使用して、それを囲む要素から完全修飾クラス名を取得します。これは、setterメソッドの場合はソースクラス自体のように見えます。
String className = ((TypeElement) setters.get(0)
.getEnclosingElement()).getQualifiedName().toString();
ビルダークラスを生成するために必要な最後の情報は、セッターの名前とそれらの引数の型の名前の間のマップです。
Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));
6.3. 出力ファイルの生成
これで、ビルダークラスを生成するために必要な情報がすべて揃いました。ソースクラスの名前、そのすべてのセッターの名前、そしてそれらの引数の型です。
出力ファイルを生成するには、
AbstractProcessor.processingEnv
protectedプロパティのオブジェクトによって再度提供される
Filer
インスタンスを使用します。
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
//writing generated file to out ...
}
writeBuilderFile
メソッドの完全なコードを以下に示します。ソースクラスとビルダークラスのパッケージ名、完全修飾ビルダークラス名、および単純クラス名を計算するだけです。
残りのコードはかなり簡単です。
private void writeBuilderFile(
String className, Map<String, String> setterMap)
throws IOException {
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String builderClassName = className + "Builder";
String builderSimpleClassName = builderClassName
.substring(lastDot + 1);
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
setterMap.entrySet().forEach(setter -> {
String methodName = setter.getKey();
String argumentType = setter.getValue();
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});
out.println("}");
}
}
** 7. 例の実行
実際のコード生成を確認するには、両方のモジュールを共通の親ルートからコンパイルするか、まず
annotation-processor
モジュールをコンパイルしてから
annotation-user
モジュールをコンパイルします。
生成された
PersonBuilder
クラスは、
annotation-user/target/generated-sources/annotations/com/baeldung/annotation/PersonBuilder.java
ファイル内にあり、次のようになります。
package com.baeldung.annotation;
public class PersonBuilder {
private Person object = new Person();
public Person build() {
return object;
}
public PersonBuilder setName(java.lang.String value) {
object.setName(value);
return this;
}
public PersonBuilder setAge(int value) {
object.setAge(value);
return this;
}
}
8プロセッサを登録する別の方法
注釈プロセッサをコンパイル段階で使用するには、ユースケースと使用するツールに応じて、他にもいくつかのオプションがあります。
8.1. 注釈プロセッサツールを使用する
apt
ツールは、ソースファイルを処理するための特別なコマンドラインユーティリティです。これはJava 5の一部でしたが、Java 7以降、他のオプションを支持するために非推奨となり、Java 8では完全に削除されました。
8.2. コンパイラキーの使い方
-processor
コンパイラキーは、独自の注釈プロセッサを使用してコンパイラのソース処理段階を強化するための標準JDK機能です。
プロセッサ自体と注釈は、別々のコンパイルでクラスとしてすでにコンパイルされていて、クラスパスに存在している必要があるので注意してください。
javac com/baeldung/annotation/processor/BuilderProcessor
javac com/baeldung/annotation/processor/BuilderProperty
次に、コンパイルしたばかりの注釈プロセッサクラスを指定する
-processor
キーを使用して、実際のソースのコンパイルを行います。
javac -processor com.baeldung.annotation.processor.MyProcessor Person.java
複数の注釈プロセッサを一度に指定するには、次のようにそれらのクラス名をカンマで区切ります。
javac -processor package1.Processor1,package2.Processor2 SourceFile.java
8.3. Mavenを使う
maven-compiler-plugin
はその構成の一部として注釈プロセッサを指定することを可能にします。
これは、コンパイラプラグインに注釈プロセッサを追加する例です。
generatedSourcesDirectory
構成パラメーターを使用して、生成されたソースを入れるディレクトリーを指定することもできます。
BuilderProcessor
クラスはコンパイル済みである必要があることに注意してください。たとえば、ビルドの依存関係にある別のjarからインポートします。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<generatedSourcesDirectory>${project.build.directory}
/generated-sources/</generatedSourcesDirectory>
<annotationProcessors>
<annotationProcessor>
com.baeldung.annotation.processor.BuilderProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
8.4. クラスパスへのプロセッサjarの追加
コンパイラオプションで注釈プロセッサを指定する代わりに、プロセッサクラスを持つ特別に構造化されたjarをコンパイラのクラスパスに単純に追加することができます。
自動的に拾うために、コンパイラはプロセッサクラスの名前を知っていなければなりません。そのため、
META-INF/services/javax.annotation.processing.Processor
ファイルでプロセッサの完全修飾クラス名として指定する必要があります。
com.baeldung.annotation.processor.BuilderProcessor
また、このjarファイルから複数のプロセッサを指定して、それらを新しい行で区切って自動的に選択することもできます。
package1.Processor1
package2.Processor2
package3.Processor3
Mavenを使用してこのjarファイルをビルドし、このファイルを
src/main/resources/META-INF/services
ディレクトリに直接配置しようとすると、次のエラーが発生します。
----[ERROR]Bad service configuration file, or exception thrown while
constructing Processor object: javax.annotation.processing.Processor:
Provider com.baeldung.annotation.processor.BuilderProcessor not found
----
これは、
BuilderProcessor
ファイルがまだコンパイルされていないときに、コンパイラがモジュール自体の
source-processing
ステージ中にこのファイルを使おうとするためです。ファイルはMavenビルドのリソースコピー段階で別のリソースディレクトリ内に置かれ
META-INF/services
ディレクトリにコピーされるか、ビルド中に生成されなければなりません。
次のセクションで説明するGoogleの
auto-service
ライブラリでは、簡単な注釈を使ってこのファイルを生成できます。
8.5. Google
自動サービス
ライブラリを使用する
登録ファイルを自動的に生成するには、Googleの
auto-service
ライブラリにある
@ AutoService
アノテーションを次のように使用します。
@AutoService(Processor.class)
public BuilderProcessor extends AbstractProcessor {
//...
}
この注釈自体は、オートサービスライブラリからの注釈プロセッサによって処理されます。このプロセッサは、
BuilderProcessor
クラス名を含む
META-INF/services/javax.annotation.processing.Processor
ファイルを生成します。
9結論
この記事では、POJOのBuilderクラスを生成する例を使用して、ソースレベルの注釈処理を説明しました。私たちはあなたのプロジェクトに注釈プロセッサを登録するいくつかの代替方法も提供しました。
この記事のソースコードはhttps://github.com/eugenp/tutorials/tree/master/annotations[on GitHub]にあります。