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]にあります。