Javaアノテーション処理とビルダーの作成

Javaアノテーションの処理とビルダーの作成

1. 前書き

この記事はan intro to Java source-level annotation processingであり、コンパイル中に追加のソースファイルを生成するためにこの手法を使用する例を提供します。

2. 注釈処理のアプリケーション

ソースレベルの注釈処理は、Java 5で初めて登場しました。 これは、コンパイル段階で追加のソースファイルを生成するための便利な手法です。

ソースファイルはJavaファイルである必要はありません。ソースコードの注釈に基づいて、あらゆる種類の説明、メタデータ、ドキュメント、リソース、またはその他の種類のファイルを生成できます。

アノテーション処理は、多くのユビキタスJavaライブラリで積極的に使用されています。たとえば、QueryDSLやJPAでメタクラスを生成したり、Lombokライブラリの定型コードでクラスを補強したりします。

注意すべき重要なことはthe limitation of the annotation processing API — it can only be used to generate new files, not to change existing onesです。

注目すべき例外は、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リポジトリにあります。


    1.0-rc2
    
      3.5.1
    




    
        com.google.auto.service
        auto-service
        ${auto-service.version}
        provided
    




    

        
            org.apache.maven.plugins
            maven-compiler-plugin
            ${maven-compiler-plugin.version}
            
                1.8
                1.8
            
        

    

注釈付きソースを含むannotation-user Mavenモジュールは、依存関係セクションで注釈プロセッサモジュールへの依存関係を追加することを除いて、特別な調整は必要ありません。


    com.example
    annotation-processing
    1.0.0-SNAPSHOT

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. Processorの実装

6.1. AbstractProcessorサブクラスの作成

annotation-processor Mavenモジュール内のAbstractProcessorクラスを拡張することから始めます。

まず、このプロセッサが処理できる注釈と、サポートされているソースコードバージョンを指定する必要があります。 これは、ProcessorインターフェースのメソッドgetSupportedAnnotationTypesおよびgetSupportedSourceVersionを実装するか、クラスに@SupportedAnnotationTypesおよび@SupportedSourceVersionアノテーションを付けることによって実行できます。

@AutoServiceアノテーションはauto-serviceライブラリの一部であり、次のセクションで説明するプロセッサメタデータを生成できます。

@SupportedAnnotationTypes(
  "com.example.annotation.processor.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set annotations,
      RoundEnvironment roundEnv) {
        return false;
    }
}

具体的なアノテーションクラス名だけでなく、com.example.annotationパッケージとそのすべてのサブパッケージ内のアノテーションを処理するための“com.example.annotation.、またはすべてのアノテーションを処理するための“”などのワイルドカードも指定できます。

実装する必要がある唯一のメソッドは、処理自体を実行するprocessメソッドです。 一致する注釈を含むすべてのソースファイルに対してコンパイラによって呼び出されます。

注釈は最初のSet<? extends TypeElement> annotations引数として渡され、現在の処理ラウンドに関する情報はRoundEnviroment roundEnv引数として渡されます。

アノテーションプロセッサが渡されたすべてのアノテーションを処理し、リストの下にある他のアノテーションプロセッサに渡されたくない場合、返されるboolean値はtrueである必要があります。

6.2. 情報収集中

私たちのプロセッサはまだ実際には何も役に立たないので、コードで埋めましょう。

まず、クラスで見つかったすべてのアノテーションタイプを反復処理する必要があります。この場合、このアノテーションが発生した場合でも、annotationsセットには@BuilderPropertyアノテーションに対応する単一の要素が含まれます。ソースファイルで複数回。

それでも、完全を期すために、processメソッドを反復サイクルとして実装することをお勧めします。

@Override
public boolean process(Set annotations,
  RoundEnvironment roundEnv) {

    for (TypeElement annotation : annotations) {
        Set annotatedElements
          = roundEnv.getElementsAnnotatedWith(annotation);

        // …
    }

    return true;
}

このコードでは、RoundEnvironmentインスタンスを使用して、@BuilderPropertyアノテーションが付けられたすべての要素を受け取ります。 Personクラスの場合、これらの要素はsetNameおよびsetAgeメソッドに対応します。

@BuilderPropertyアノテーションのユーザーは、実際にはセッターではないメソッドに誤ってアノテーションを付ける可能性があります。 セッターメソッド名はsetで始まり、メソッドは単一の引数を受け取る必要があります。 小麦をもみ殻から分けましょう。

次のコードでは、Collectors.partitioningBy()コレクターを使用して、注釈付きメソッドを2つのコレクションに分割します。正しく注釈が付けられたセッターと他の誤って注釈が付けられたメソッドです。

Map> annotatedMethods = annotatedElements.stream().collect(
  Collectors.partitioningBy(element ->
    ((ExecutableType) element.asType()).getParameterTypes().size() == 1
    && element.getSimpleName().toString().startsWith("set")));

List setters = annotatedMethods.get(true);
List 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;
}

セッターコレクションに少なくとも1つの要素がある場合、それを使用して、囲んでいる要素から完全修飾クラス名を取得します。

String className = ((TypeElement) setters.get(0)
  .getEnclosingElement()).getQualifiedName().toString();

ビルダークラスを生成するために必要な最後の情報は、セッターの名前と引数タイプの名前の間のマップです。

Map 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 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/example/annotation/PersonBuilder.javaファイル内にあり、次のようになります。

package com.example.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/example/annotation/processor/BuilderProcessor
javac com/example/annotation/processor/BuilderProperty

次に、コンパイルしたばかりのアノテーションプロセッサクラスを指定する-processorキーを使用して、ソースの実際のコンパイルを行います。

javac -processor com.example.annotation.processor.MyProcessor Person.java

複数の注釈プロセッサを一度に指定するには、次のようにクラス名をカンマで区切ることができます。

javac -processor package1.Processor1,package2.Processor2 SourceFile.java

8.3. Mavenを使用する

maven-compiler-pluginを使用すると、構成の一部として注釈プロセッサを指定できます。

コンパイラプラグインに注釈プロセッサを追加する例を次に示します。 generatedSourcesDirectory構成パラメーターを使用して、生成されたソースを配置するディレクトリーを指定することもできます。

BuilderProcessorクラスはすでにコンパイルされている必要があることに注意してください。たとえば、ビルドの依存関係にある別のjarからインポートします。


    

        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.5.1
            
                1.8
                1.8
                UTF-8
                ${project.build.directory}
                  /generated-sources/
                
                    
                        com.example.annotation.processor.BuilderProcessor
                    
                
            
        

    

8.4. クラスパスへのプロセッサJarの追加

コンパイラオプションで注釈プロセッサを指定する代わりに、特別に構造化されたjarをプロセッサクラスとともにコンパイラのクラスパスに追加するだけです。

自動的に取得するには、コンパイラはプロセッサクラスの名前を知っている必要があります。 したがって、META-INF/services/javax.annotation.processing.Processorファイルでプロセッサの完全修飾クラス名として指定する必要があります。

com.example.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.example.annotation.processor.BuilderProcessor not found

これは、BuilderProcessorファイルがまだコンパイルされていないときに、コンパイラがモジュール自体のsource-processingステージでこのファイルを使用しようとするためです。 ファイルは、別のリソースディレクトリ内に配置して、Mavenビルドのリソースコピー段階でMETA-INF/servicesディレクトリにコピーするか、(さらに適切に)ビルド中に生成する必要があります。

次のセクションで説明するGoogleauto-serviceライブラリでは、簡単な注釈を使用してこのファイルを生成できます。

8.5. Googleauto-serviceライブラリの使用

登録ファイルを自動的に生成するには、次のように、Googleのauto-serviceライブラリから@AutoServiceアノテーションを使用できます。

@AutoService(Processor.class)
public BuilderProcessor extends AbstractProcessor {
    // …
}

この注釈自体は、自動サービスライブラリの注釈プロセッサによって処理されます。 このプロセッサは、BuilderProcessorクラス名を含むMETA-INF/services/javax.annotation.processing.Processorファイルを生成します。

9. 結論

この記事では、POJOのBuilderクラスを生成する例を使用して、ソースレベルの注釈処理を示しました。 また、プロジェクトに注釈プロセッサを登録するいくつかの代替方法も提供しています。

記事のソースコードはon GitHubで入手できます。