Java注釈処理とビルダーの作成
1. 序章
この記事はJavaソースレベルの注釈処理の紹介であり、コンパイル中に追加のソースファイルを生成するためにこの手法を使用する例を提供します。
2. 注釈処理のアプリケーション
ソースレベルの注釈処理は、Java5で最初に登場しました。 これは、コンパイル段階で追加のソースファイルを生成するための便利な手法です。
ソースファイルはJavaファイルである必要はありません。ソースコードの注釈に基づいて、あらゆる種類の説明、メタデータ、ドキュメント、リソース、またはその他の種類のファイルを生成できます。
アノテーション処理は、多くのユビキタスJavaライブラリで積極的に使用されています。たとえば、QueryDSLやJPAでメタクラスを生成したり、Lombokライブラリの定型コードでクラスを拡張したりします。
注意すべき重要な点は、注釈処理APIの制限です。これは、既存のファイルを変更するのではなく、新しいファイルを生成するためにのみ使用できます。
注目すべき例外は、 Lombok ライブラリです。これは、ブートストラップメカニズムとして注釈処理を使用して、コンパイルプロセスに自身を組み込み、一部の内部コンパイラAPIを介してASTを変更します。 このハッキーな手法は、注釈処理の意図された目的とは関係がないため、この記事では説明しません。
3. 注釈処理API
注釈処理は複数のラウンドで行われます。 各ラウンドは、コンパイラがソースファイル内の注釈を検索し、これらの注釈に適した注釈プロセッサを選択することから始まります。 次に、各注釈プロセッサは、対応するソースで呼び出されます。
このプロセス中にファイルが生成された場合、生成されたファイルを入力として別のラウンドが開始されます。 このプロセスは、処理段階で新しいファイルが生成されなくなるまで続きます。
次に、各注釈プロセッサは、対応するソースで呼び出されます。 このプロセス中にファイルが生成された場合、生成されたファイルを入力として別のラウンドが開始されます。 このプロセスは、処理段階で新しいファイルが生成されなくなるまで続きます。
アノテーション処理APIは、javax.annotation.processingパッケージにあります。 実装する必要のあるメインインターフェイスはProcessorインターフェイスであり、AbstractProcessorクラスの形式で部分的に実装されています。 このクラスは、独自の注釈プロセッサを作成するために拡張するクラスです。
4. プロジェクトの設定
アノテーション処理の可能性を示すために、アノテーション付きクラスの流暢なオブジェクトビルダーを生成するためのシンプルなプロセッサーを開発します。
プロジェクトを2つのMavenモジュールに分割します。 それらの1つであるannotation-processorモジュールには、プロセッサ自体が注釈とともに含まれ、もう1つである annotation-user モジュールには、注釈付きクラスが含まれます。 これは、注釈処理の一般的な使用例です。
annotation-processorモジュールの設定は次のとおりです。 Googleのauto-serviceライブラリを使用して、後で説明するプロセッサメタデータファイルを生成し、maven-compiler-pluginをJava8ソース用に調整します。コード。 これらの依存関係のバージョンは、プロパティセクションに抽出されます。
auto-serviceライブラリとmaven-compiler-pluginの最新バージョンは、MavenCentralリポジトリにあります。
<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 セッターメソッドによって完全に定義されているため、世代にとって明らかな選択です。
セッターメソッドの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. プロセッサの実装
6.1. AbstractProcessorサブクラスの作成
まず、 annotation-processorMavenモジュール内の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メソッドです。 これは、一致する注釈を含むすべてのソースファイルに対してコンパイラによって呼び出されます。
注釈は最初に渡されます
注釈プロセッサが渡されたすべての注釈を処理し、リストの下にある他の注釈プロセッサに渡されたくない場合、返される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保護フィールドからアクセスできるMessagerインスタンスを使用しましょう。 次の行は、ソース処理段階で誤って注釈が付けられた要素ごとにエラーを出力します。
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BuilderProperty must be applied to a setXxx method "
+ "with a single argument", element));
もちろん、正しいセッターコレクションが空の場合、現在のタイプ要素セットの反復を続行する意味はありません。
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保護プロパティのオブジェクトによって再度提供される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 ツールは、ソースファイルを処理するための特別なコマンドラインユーティリティでした。 これはJava5の一部でしたが、Java 7以降、他のオプションのために非推奨になり、Java8で完全に削除されました。 この記事では説明しません。
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 ディレクトリにコピーするか、ビルド中に(さらに適切に)生成する必要があります。
次のセクションで説明するGoogleauto-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クラスを生成する例を使用して、ソースレベルのアノテーション処理を示しました。 また、プロジェクトに注釈プロセッサを登録するためのいくつかの代替方法も提供しています。
この記事のソースコードは、GitHubでから入手できます。