Pluggable Annotation Processing API 使い方メモ

  • 91
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Pluggable Annotation Processing API とは

Java 1.6 から追加された、コンパイル時にアノテーションを処理するための仕組み。

Lombok とか JPA の Metamodel とかで利用されている。
つまり、コンパイル時にアノテーションを読み込んでソースコードを自動生成したりできる。

Java 1.5 でアノテーションが追加されたときに、同様にアノテーションをコンパイル時に処理する仕組みとして Annotation Processing Tool (apt) が追加されていたが、これとは別物らしい。

Hello World

プロセッサーを実装する

MyAnnotationProcessor.java
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;

import javax.lang.model.element.TypeElement;
import javax.lang.model.SourceVersion;

import java.util.Set;
import java.util.HashSet;

public class MyAnnotationProcessor extends AbstractProcessor {

    private int round = 1;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        System.out.println("Round : " + (this.round++));
        annotations.forEach(System.out::println);
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedAnnotationTypes = new HashSet<>();
        supportedAnnotationTypes.add("*");

        return supportedAnnotationTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }
}

処理対象のソースを作る

Hoge.java
public class Hoge {
    @Deprecated
    public void deprecatedMethod() {}

    @Override
    public String toString() {
        return "hoge";
    }
}

プロセッサーをコンパイルする

> javac MyAnnotationProcessor.java

> dir /b
MyAnnotationProcessor.class
MyAnnotationProcessor.java
Hoge.java 

プロセッサーを指定してコードをコンパイルする

> javac -processor MyAnnotationProcessor Hoge.java
Round : 1
java.lang.Deprecated
java.lang.Override
Round : 2

> dir /b
Hoge.class
Hoge.java
MyAnnotationProcessor.class
MyAnnotationProcessor.java

説明

プロセッサーの作成

MyAnnotationProcessor.java
import javax.annotation.processing.AbstractProcessor;

public class MyAnnotationProcessor extends AbstractProcessor {
  • アノテーションプロセッサーを作るには、 Processor を実装したクラスを用意する。
  • 標準で Processor を実装した抽象クラス AbstractProcessor が用意されているので、普通はそれを継承して作る。

処理対象を定義する

MyAnnotationProcessor.java
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedAnnotationTypes = new HashSet<>();
        supportedAnnotationTypes.add("*");

        return supportedAnnotationTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }
  • getSupportedAnnotationTypes() で、処理するアノテーションを定義する。
    • 戻り値の Set<String> では、処理対象となるアノテーションのパターンを返すようにする。
    • 通常は、 FQCN を指定する。
    • * を使ったワイルドカード表現も可能。
    • * だけの場合は、全てのアノテーションが処理対象になる。
  • getSupportedSourceVersion() は、サポート限界となるバージョンを定義する。
    • SourceVersion 列挙型で指定する。
    • サポートバージョンより新しい JDK を使うと、警告メッセージが表示されるようになる。
SourceVersion.RELEASE_7を返すようにした場合の警告メセージ例
警告: 注釈プロセッサ'MyAnnotationProcessor'から-source '1.8'より小さいソース・バージョン'RELEASE_7'がサポートされています

コンパイル時にプロセッサーを指定する

> javac -processor MyAnnotationProcessor Hoge.java
  • -processor オプションで、使用するプロセッサーを指定する。

ラウンド

実行結果
Round : 1
java.lang.Deprecated
java.lang.Override
Round : 2
  • プロセッサーによる処理(process)の呼び出しは、複数回行われる。
  • それぞれの処理を ラウンド と呼ぶ。
  • 1回のラウンドの処理でソースコードを新規に生成したりすると、生成されたソースに使用されているアノテーションを処理するため、もう一度ラウンドが実行される。
  • ソースの生成などをしなくても、最低2回はラウンドが実行される。

サポートバージョンと処理対象の絞り込みをアノテーションで指定する

MyAnnotationProcessor.java
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;

import javax.lang.model.element.TypeElement;
import javax.lang.model.SourceVersion;

import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class MyAnnotationProcessor extends AbstractProcessor {

    private int round = 1;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        System.out.println("Round : " + (this.round++));
        annotations.forEach(System.out::println);
        return true;
    }
}
  • メソッドの変わりに、 @SupportedSourceVersion@SupportedAnnotationTypes アノテーションで定義することができる。

アノテーションの情報を取得する

MyAnnotationProcessor.java
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;

import javax.lang.model.element.TypeElement;
import javax.lang.model.element.Element;
import javax.lang.model.SourceVersion;

import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> typeElements, RoundEnvironment env) {

        for (TypeElement typeElement : typeElements) {

            for (Element element : env.getElementsAnnotatedWith(typeElement)) {

                Override override = element.getAnnotation(Override.class);

                if (override != null) {
                    System.out.println("@Override at " + element);
                }
            }
        }

        return true;
    }
}
動作確認
> javac -processor MyAnnotationProcessor Hoge.java
@Override at toString()
  • RoundEnvironment#getElementsAnnotatedWith(TypeElement) メソッドで、実際にアノテートされている場所を表す ElementSet を取得できる。
  • Element#getAnnotation(Class) メソッドで、実際のアノテーションを取得できる。

メッセージ出力

基本

MyAnnotationProcessor.java
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Messager messager = super.processingEnv.getMessager();

        messager.printMessage(Kind.OTHER, "Other");
        messager.printMessage(Kind.NOTE, "Note");
        messager.printMessage(Kind.WARNING, "Warning");
        messager.printMessage(Kind.MANDATORY_WARNING, "Mandatory Warning");
        messager.printMessage(Kind.ERROR, "Error");

        return true;
    }
}
コンパイル実行結果
注意:Other
注意:Note
警告: Warning
警告: Mandatory Warning
エラー: Error
注意:Other
注意:Note
警告: Warning
警告: Mandatory Warning
エラー: Error
エラー2個
警告4個
  • ※2回ずつ出力されているのは、ラウンドが2回実行されているため。
  • AbstractProcessor クラスが持つ processingEnv というフィールドから、 getMessager() メソッドを使って Messager のインスタンスを取得する。
  • Messager#printMessage() メソッドを使って、コンパイル時のメッセージ出力ができる。
  • 第一引数にメッセージの種類を指定する(Diagnostic.Kind)。
  • 第二引数に出力するメッセージを渡す。
  • 種類を Kind.ERROR にしてメッセージを出力すると、コンパイル結果がエラーになる。

アノテートされたコード上の位置情報を出力する

MyAnnotationProcessor.java
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Messager messager = super.processingEnv.getMessager();

        for (TypeElement typeElement : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {
                messager.printMessage(Kind.NOTE, "hogehoge", element);
            }
        }

        return true;
    }
}
コンパイル実行結果
Hoge.java:5: 注意:hogehoge
    public String toString() {
                  ^
  • Messager#printMessage() メソッドの第三引数に Element を渡すと、その要素のコード上の位置情報が一緒に出力される。

アノテーションの位置情報を出力する

MyAnnotationProcessor.java
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes("java.lang.Override")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Messager messager = super.processingEnv.getMessager();

        for (TypeElement typeElement : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {
                AnnotationMirror override = element.getAnnotationMirrors().get(0);
                messager.printMessage(Kind.NOTE, "hogehoge", element, override);
            }
        }

        return true;
    }
}
コンパイル実行結果
Hoge.java:4: 注意:hogehoge
    @Override
    ^
  • Messager#printMessage() メソッドの第四引数に AnnotationMirror を渡すと、そのアノテーションのコード上の位置情報が一緒に出力される。
  • 第五引数に AnnotationValue を渡せば、アノテーションの引数の位置情報も出力できそう(試してない)。

アノテートされている要素の情報を取得する

MyAnnotation.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target({
    ElementType.TYPE, ElementType.METHOD, ElementType.LOCAL_VARIABLE,
    ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_PARAMETER
})
public @interface MyAnnotation {

    String value();
}
MyAnnotationProcessor.java
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

@SupportedAnnotationTypes("MyAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement typeElement : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {
                MyAnnotation anno = element.getAnnotation(MyAnnotation.class);

                String msg =
                    "<<@MyAnnotation(\"" + anno.value() + "\")>>\n"
                  + "  Kind : " + element.getKind() + "\n"
                  + "  SimpleName : " + element.getSimpleName() + "\n"
                  + "  Modifiers : " + element.getModifiers() + "\n"
                  + "  asType : " + element.asType() + "\n"
                  + "  EnclosedElements : " + element.getEnclosedElements() + "\n"
                  + "  EnclosingElement : " + element.getEnclosingElement() + "\n"
                  + "  AnnotationMirrors : " + element.getAnnotationMirrors() + "\n"
                  ;

                System.out.println(msg);
            }
        }

        return true;
    }
}
Hoge.java
@MyAnnotation("クラス")
public class Hoge<@MyAnnotation("型引数") T> {

    @MyAnnotation("フィールド")
    private double d;

    @MyAnnotation("メソッド")
    public void method(@MyAnnotation("引数") int i) {
        @MyAnnotation("ローカル変数")
        String str;
    }
}
コンパイル実行結果
<<@MyAnnotation("クラス")>>
  Kind : CLASS
  SimpleName : Hoge
  Modifiers : [public]
  asType : Hoge<T>
  EnclosedElements : Hoge(),d,method(int)
  EnclosingElement : 名前のないパッケージ
  AnnotationMirrors : @MyAnnotation("\u30af\u30e9\u30b9")

<<@MyAnnotation("型引数")>>
  Kind : TYPE_PARAMETER
  SimpleName : T
  Modifiers : []
  asType : T
  EnclosedElements :
  EnclosingElement : Hoge
  AnnotationMirrors : @MyAnnotation("\u578b\u5f15\u6570")

<<@MyAnnotation("フィールド")>>
  Kind : FIELD
  SimpleName : d
  Modifiers : [private]
  asType : double
  EnclosedElements :
  EnclosingElement : Hoge
  AnnotationMirrors : @MyAnnotation("\u30d5\u30a3\u30fc\u30eb\u30c9")

<<@MyAnnotation("メソッド")>>
  Kind : METHOD
  SimpleName : method
  Modifiers : [public]
  asType : (int)void
  EnclosedElements :
  EnclosingElement : Hoge
  AnnotationMirrors : @MyAnnotation("\u30e1\u30bd\u30c3\u30c9")

<<@MyAnnotation("引数")>>
  Kind : PARAMETER
  SimpleName : i
  Modifiers : []
  asType : int
  EnclosedElements :
  EnclosingElement : method(int)
  AnnotationMirrors : @MyAnnotation("\u5f15\u6570")
  • Element インスタンスから、アノテートされた要素の情報をいろいろ取得できる。

jar にパッケージして -processor オプション無しでも使えるようにする

毎回 javac のオプションで -processor を指定するのは疲れる。
アノテーションプロセッサーを所定の方法で jar にパッケージすると、コンパイル時に jar をクラスパスに指定するだけでプロセッサーの処理を挟むことができるようになる。

プロセッサーを含む jar を作成する

MyOverrideProcessor.java
package sample.processor;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

@SupportedAnnotationTypes("java.lang.Override")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyOverrideProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("Override!!");
        return true;
    }
}

@Override アノテーションを処理するプロセッサー。

これを、以下の構成になるように jar にパッケージする。

|-sample/processor/
|  `-MyOverrideProcessor.class
`-META-INF/services/
   `-javax.annotation.processing.Processor

この META-INF/services/javax.annotation.processing.Processor はただのテキストファイルで、内容は以下のように使用するプロセッサーの FQCN を記述しておく。

javax.annotation.processing.Processor
sample.processor.MyOverrideProcessor

この jar を仮に processor.jar とする。

実行する

Hoge.java
public class Hoge {
    @Override
    public String toString() {
        return super.toString();
    }
}
> javac -cp processor.jar Hoge.java
Override!!
Override!!

説明

  • アノテーションプロセッサーを含む jar ファイルの中に、 META-INF/services/javax.annotation.processing.Processor という形でテキストファイルを配置する。
  • javax.annotation.processing.Processor の中には、使用するプロセッサーの FQCN を記述する。
  • この jar をクラスパスに含めた状態でコンパイルを実行すると、 javax.annotation.processing.Processor で指定されたプロセッサーがコンパイル時に利用される。
  • javax.annotation.processing.Processor には、改行区切りで複数のプロセッサーを記述することができる。
  • この META-INF/services の下に使用するクラスの情報を配置するという方法は、 Java の ServiceLoader という仕組みを利用したものと思われる。

Eclipse で利用する

基本

プロセッサー側のプロジェクトを作成する

processor.JPG

MyAnnotationProcessor.java
package sample.processor;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Messager messager = super.processingEnv.getMessager();
        messager.printMessage(Kind.NOTE, "Hello Annotation Processor!!");

        return true;
    }
}
javax.annotation.processing.Processor
sample.processor.MyAnnotationProcessor
  • 普通に java プロジェクトを作る。
  • ServiceLoader を利用した形式にする(javax.annotation.processing.Processor)。

jar ファイルを出力する

  • プロジェクトを右クリックして、 [エクスポート] を選択する。
  • [Java] > [JAR ファイル] を選択し、 [次へ] をクリック。
  • 任意の場所に jar ファイルを出力する。

プロセッサーを利用する側のプロジェクトを作成する

processor.JPG

Hoge.java
public class Hoge {

    @Override
    public String toString() {
        return super.toString();
    }
}
  • 普通に java プロジェクトを作成する。

注釈処理を有効にする

  • プロジェクトを右クリックして、 [プロパティー] を選択。
  • [Java コンパイラー] > [注釈処理] を選択。
  • [プロジェクト固有の設定を可能にする] にチェックを入れる。
  • 続いて、 [注釈処理を使用可能にする] と [エディターでの処理を使用可能にする] のチェックを入れる。

processor.JPG

  • さらに、 [注釈処理] > [ファクトリー・パス] を選択する。
  • [プロジェクト固有の設定を可能にする] にチェックを入れる。
  • [外部 Jar 追加] をクリックし、先ほど生成した MyAnnotationProcessor プロジェクトの jar ファイルを選択する。

processor.JPG

これで、注釈処理が有効になる。

出力結果を確認する

  • [ウィンドウ] > [ビューの表示] > [エラー・ログ] を選択し、「エラー・ログ」ウィンドウを表示させる。

processor.JPG

  • うまく処理されていると、 Messager で出力した内容が表示されている。
  • Eclipse で動かす場合、標準出力(System.out)で出力した内容は確認できないので、 Messager で出力して「エラー・ログ」ウィンドウで確認する。
  • プロセッサー側の実装を修正した場合は、 jar を出力しなおした上で利用側の「ファクトリー・パス」の設定をし直す(一回外して、再度設定し直す)必要がある。

警告やエラーのメッセージはエディター上に表示される

MyAnnotationProcessor.java
package sample.processor;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes("java.lang.Override")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Messager messager = super.processingEnv.getMessager();

        for (TypeElement typeElement : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {
                messager.printMessage(Kind.ERROR, "オーバーライドさせぬ!", element);
            }
        }

        return true;
    }
}

利用側の様子

processor.JPG

エディター上でエラー表示されてる!

Gradle で使用する

基本

フォルダ構成

フォルダ構成
|-settings.gradle
|-build.gradle
|-processor/
|  `-src/main/
|     |-java/sample/processor/
|     |  `-MyAnnotationProcessor.java
|     `-resources/META-INF/services/
|        `-javax.annotation.processing.Processor
|
`-client/
   |-build.gradle
   `-src/main/java/sample/processor/
      `-Hoge.java
  • マルチプロジェクトの構成。
  • processorclient というプロジェクトがある。
    • processor プロジェクトでは、アノテーションプロセッサーを実装する。
    • client プロジェクトでは、 processor プロジェクトで作成したアノテーションプロセッサーを利用する。

実装等

settings.gradle
include 'processor', 'client'
build.gradle
subprojects {
    apply plugin: 'java'
}
client/build.gradle
dependencies {
    compile project(':processor')
}
MyAnnotationProcessor.java
package sample.processor;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("Hello!!");
        return true;
    }
}
Hoge.java
package sample.processor;

public class Hoge {
    @Override
    public String toString() {
        return "hoge";
    }
}

動作確認

> gradle compileJava
:processor:compileJava
:processor:processResources
:processor:classes
:processor:jar
:client:compileJava
Hello!!
Hello!!

BUILD SUCCESSFUL

Total time: 3.423 secs
  • ServiceLoader を使う形式にしておけば、普通に利用できた。

Eclipse プロジェクト化

Gradle プラグイン作りました

2016/01/08 追記

以下の設定ファイルを自動生成する Gradle プラグインを作りました。


gradle eclipse したら、自動で注釈処理の設定が有効になるようにする。

フォルダ構成

フォルダ構成
|-build.gradle
|-lib/
|  `-processor.jar
`-src/main/java/sample/processor/
   `-Hoge.java

プロセッサーをパッケージングした jar ファイルは、 lib フォルダの下の配置しておく。

build.gradle

client/build.gradle
apply plugin: 'java'
apply plugin: 'eclipse'

ext {
    eclipseAptPrefsFile = '.settings/org.eclipse.jdt.apt.core.prefs'
    eclipseFactoryPathFile = '.factorypath'
    processorJarPath = 'lib/processor.jar'
}

dependencies {
    compile files(processorJarPath)
}

eclipse {
    project.name = 'MyAnnotationProcessorClient'

    classpath.file.withXml {
        it.asNode().appendNode('classpathentry', [kind: 'src', path: '.apt_generated'])
    }

    jdt.file.withProperties { properties ->
        properties.put 'org.eclipse.jdt.core.compiler.processAnnotations', 'enabled'
    }
}

eclipseJdt << {
    file(eclipseAptPrefsFile).write """\
        |eclipse.preferences.version=1
        |org.eclipse.jdt.apt.aptEnabled=true
        |org.eclipse.jdt.apt.genSrcDir=.apt_generated
        |org.eclipse.jdt.apt.reconcileEnabled=true
        |""".stripMargin()

    file(eclipseFactoryPathFile).write """\
        |<factorypath>
        |    <factorypathentry kind="PLUGIN" id="org.eclipse.jst.ws.annotations.core" enabled="true" runInBatchMode="false"/>
        |    <factorypathentry kind="EXTJAR" id="${file(processorJarPath).absolutePath}" enabled="true" runInBatchMode="false"/>
        |</factorypath>
        |""".stripMargin()
}

cleanEclipse << {
    file(eclipseAptPrefsFile).delete()
    file(eclipseFactoryPathFile).delete()
}

説明

やっていることは単純で、 eclipse タスクを実行したら注釈処理を有効化するのに必要となる設定ファイルを追加したりしている。

org.eclipse.jdt.apt.core.prefs の作成

ext {
    eclipseAptPrefsFile = '.settings/org.eclipse.jdt.apt.core.prefs'
    ...
}

...

eclipseJdt << {
    file(eclipseAptPrefsFile).write """\
        |eclipse.preferences.version=1
        |org.eclipse.jdt.apt.aptEnabled=true
        |org.eclipse.jdt.apt.genSrcDir=.apt_generated
        |org.eclipse.jdt.apt.reconcileEnabled=true
        |""".stripMargin()

...

.factorypath の作成

ext {
    eclipseFactoryPathFile = '.factorypath'
    ...
}

...

eclipseJdt << {
    ...

    file(eclipseFactoryPathFile).write """\
        |<factorypath>
        |    <factorypathentry kind="PLUGIN" id="org.eclipse.jst.ws.annotations.core" enabled="true" runInBatchMode="false"/>
        |    <factorypathentry kind="EXTJAR" id="${file(processorJarPath).absolutePath}" enabled="true" runInBatchMode="false"/>
        |</factorypath>
        |""".stripMargin()
}

.classpath の編集

eclipse {
    ...

    classpath.file.withXml {
        it.asNode().appendNode('classpathentry', [kind: 'src', path: '.apt_generated'])
    }

    ...
}

org.eclipse.jdt.core.prefs の編集

eclipse {
    ...

    jdt.file.withProperties { properties ->
        properties.put 'org.eclipse.jdt.core.compiler.processAnnotations', 'enabled'
    }
}

cleanEclipse に削除対象を追加

ext {
    eclipseAptPrefsFile = '.settings/org.eclipse.jdt.apt.core.prefs'
    eclipseFactoryPathFile = '.factorypath'
    ...
}

...

cleanEclipse << {
    file(eclipseAptPrefsFile).delete()
    file(eclipseFactoryPathFile).delete()
}

Java のソースコードを生成する

アノテーションプロセッサーの処理の中で、ソースコードを動的に生成してみる。

実装

MyAnnotation.java
package sample.processor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
public @interface MyAnnotation {
    boolean value() default false;
}
Hoge.java
package sample.processor;

@MyAnnotation(true)
public class Hoge {
}
MyAnnotationProcessor.java
package sample.processor;

import java.io.IOException;
import java.io.Writer;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;

@SupportedAnnotationTypes("sample.processor.MyAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (annotations.size() == 0) {
            return true;
        }

        Messager messager = super.processingEnv.getMessager();

        for (TypeElement typeElement : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {
                MyAnnotation anno = element.getAnnotation(MyAnnotation.class);

                messager.printMessage(Kind.NOTE, element.getSimpleName() + " class is annotated by @MyAnnotation.");

                if (!anno.value()) {
                    messager.printMessage(Kind.NOTE, "@MyAnnotation value is false. no generate.");
                    break;
                }

                String src =
                    "package sample.processor.generated;\r\n"
                  + "import sample.processor.MyAnnotation;\r\n"
                  + "@MyAnnotation\r\n"
                  + "public class Fuga {\r\n"
                  + "    public void hello() {\r\n"
                  + "        System.out.println(\"Hello World!!\");\r\n"
                  + "    }\r\n"
                  + "}\r\n"
                  ;

                try {
                    Filer filer = super.processingEnv.getFiler();
                    JavaFileObject javaFile = filer.createSourceFile("Fuga");

                    try (Writer writer = javaFile.openWriter()) {
                        writer.write(src);
                    }

                    messager.printMessage(Kind.NOTE, "generate source code!!");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return true;
    }
}

動作確認

コンパイル実行結果
注意:Hoge class is annotated by @MyAnnotation.
注意:generate source code!!
注意:Fuga class is annotated by @MyAnnotation.
注意:@MyAnnotation value is false. no generate.
自動生成されたFugaクラス
package sample.processor.generated;
import sample.processor.MyAnnotation;
@MyAnnotation
public class Fuga {
    public void hello() {
        System.out.println("Hello World!!");
    }
}
  • Fuga.java は、 .class ファイルが出力されるフォルダのルートに出力された。
  • Fuga.class は、同じく .class ファイルの出力先の中に、 package 宣言で定義した通りの場所に出力された。
出力されたファイルの様子
|-Fuga.java
`-sample/processor/
   |-Hoge.class
   `-generated/
      `-Fuga.class

説明

MyAnnotationProcessor.java(一部)
try {
    Filer filer = super.processingEnv.getFiler();
    JavaFileObject javaFile = filer.createSourceFile("Fuga");

    try (Writer writer = javaFile.openWriter()) {
        writer.write(src);
    }

    messager.printMessage(Kind.NOTE, "generate source code!!");
} catch (IOException e) {
    e.printStackTrace();
}
  • Java のソースコードを生成するには、まず次の方法で JavaFileObject を取得する。
    • AbstractProcessor#processingEnvgetFiler() メソッドで Filer のインスタンスを取得する。
    • Filer#createSourceFile(String) メソッドで、 JavaFileObject のインスタンスを取得する(引数は生成するクラスの名前)。
  • JavaFileObject を取得したら、 openWriter()openOutputStream() を使ってライターやストリームを取得し、そこにソースコードを書き出す。
  • 生成されたファイルは自動的にコンパイルされる。
  • 生成したソースコードにアノテーションが含まれていた場合は、次のラウンドが実行され、再びプロセッサーの process() メソッドが実行される。
  • 同じクラスのソースコードを複数回出力するとエラーになる。
    • このため、処理すべきアノテーションが無くなったかどうかを確認して、なければすぐに処理を終了させている(判定には、 process() メソッドの第一引数のサイズを利用する)。
処理すべきアノテーションが無ければ、すぐに処理を終了させているところ
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (annotations.size() == 0) {
            return true;
        }

参考