プロジェクト

全般

プロフィール

Javaアノテーションプロセッサ

JSR 269 Pluggable Annotation Processing API として制定された標準ライブラリです。Java SE 5のaptツールに置き換わる機能で、javacのコンパイルプロセスでアノテーション処理を行う仕組みと、Java言語モデルにアクセスするAPIが提供されています。

最初の一歩

マーカーアノテーションを付けたクラスのフィールド情報を出力

最初の一歩として、マーカーアノテーション @ConfigProperties を定義し、このアノテーションを付けたクラスをコンパイルするときにクラスで定義されたフィールドの名前と型を標準出力するアノテーションプロセッサを定義します。

次の2つの成果物を生成するGradleプロジェクトを作成します。

  • [1] アノテーションライブラリ・プロセス用のJAR
  • [2] サンプルプログラム

サンプルプログラムをコンパイルするときに、[1]のJARを依存関係に含めることでアノテーションプロセスが実行され、標準出力にフィールド情報を出すという仕組みです。

Gradleプロジェクトの作成

gradle initを実行し、マルチモジュールのアプリケーションプロジェクトの雛形を生成します。

work$ mkdir hello_jsr269
work$ cd hello_jsr269
hello_jsr269$ gradle init
  :

gradle initを実行し、設定項目を、1:Application > 1: Java > 2: Application and library project > 1: Kotlin と選択し、複数モジュールからなるJavaのアプリケーションプロジェクトをDSLにkotlinを使って生成します。

このGradleプロジェクトは、デフォルトで3つのモジュール(app, utilities, list)が生成されます。

hello_jsr269/
  +- app/
  +- buildSrc/
  +- gradle/
  +- list/
  +- utilities/
  +- gradle.properties
  +- gradlew
  +- settings.gradle.kts

utilitiesディレクトリ以下を削除、listモジュールの名前をannotations に変更します。

hello_jsr269$ rm -rf utilities
hello_jsr269$ mv list annotations
hello_jsr269$ 

settings.gradle.ktsを、上述のモジュール削除・変更に合わせて修正します。

 rootProject.name = "jsr269_sample" 
-include("app", "list", "utilities")
+include("app", "annotations")

app/build.gradle.kts から utilitiesへの依存を削除、annotationsへの依存および annotationProssesorとしてannotationsへの依存を追加します。

 dependencies {
-    implementation("org.apache.commons:commons-text")
-    implementation(project(":utilities"))
+    implementation(project(":annotations"))
+    annotationProcessor(project(":annotations"))
 }

 application {
     // Define the main class for the application.
-    mainClass = "org.example.app.App" 
+    mainClass = "com.torutk.hello.app.App" 

appモジュールとannotationsモジュールのパッケージ名を org.exampleから変更します。

  • org.example.app -> com.torutk.hello.app
  • org.example.list -> com.torutk.hello.annotations

buildSrc/src/main/kotlin/buildlogic.java-common-conventions.gradle.kts から、org.apache.commonsへの依存定義を削除します。

 dependencies {
-    constraints {
-        // Define dependency versions as constraints
-        implementation("org.apache.commons:commons-text:1.12.0")
-    }
-

annotationモジュール

@ConfigPropertiesアノテーションと、アノテーションプロセッサ PropertiesProcessorを定義します。

@ConfigPropertiesアノテーション
package com.torutk.hello.annotations;

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

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ConfigProperties {
}
  • このアノテーションは、ソースファイルからコンパイルする時にアノテーションプロセッサの処理対象となるので、アノテーション情報はソースファイルにあれば十分です。そこで、RetentionPolicyはSOURCEを指定します。
  • このアノテーションは、クラスなどの型に対して付与します。そこで、ElementTypeはTYPEを指定します。
  • マーカーアノテーションのため、アノテーション定義の中身は空です。
PropertiesProcessor

アノテーションをコンパイル時に処理するために、アノテーションプロセッサとして定義します。
まずは、アノテーションプロセッサが発動した際に標準出力にメッセージを表示するだけの単純なプロセッサを記述します。

package com.torutk.hello.annotations;

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;
import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_21)
@SupportedAnnotationTypes({"com.torutk.hello.annotations.ConfigProperties"})
public class PropertiesProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("processing " + roundEnv.getRootElements());
        return false;
    }
}
  • アノテーションプロセッサは、便宜的にAbstractProcessorを継承して、メソッドprocessをオーバーライドします。
  • @SupportedSourceVersionアノテーションでJava SEのバージョンを指定します。ここでは現時点で最新のLTS版である21を指定しました。
  • @SupportedAnnotationTypesアノテーションで、このプロセッサが処理する対象のアノテーションを指定します。
  • processメソッドはコンパイル中に複数呼ばれます。引数annotationsには処理対象アノテーションが格納されています。roundEnvには、呼び出し毎の環境が格納されています。
サービスプロバイダにプロセッサを定義

アノテーションプロセッサは、javacの-procオプションで指定する方法と、JARファイルにサービスプロバイダ定義を記述して指定する方法とがあります。
ここでは、JARファイルのサービスプロバイダ定義を実装します。このJARファイルをjavacでコンパイルする際にクラスパスに指定することで、アノテーション処理が行われます。

META-INF/services/javax.annotation.processing.Processor

com.torutk.hello.annotations.PropertiesProcessor

appモジュール

アノテーション @ConfigPropertiesを指定するクラス MyProperties を定義

package com.torutk.hello.app;

import com.torutk.hello.annotations.ConfigProperties;

@ConfigProperties
class MyProperties {
    String name;
    int age;
}

h4. ビルド

gradleでビルド(gradle jar) を実施します。

hello_jsr269$ ./gradlew clean jar
  :
> Task :app:compileJava
processing [com.torutk.hello.app.App, com.torutk.hello.app.MyProperties]
processing []

アノテーションプロセッサによる処理メッセージが表示されます。

アノテーションプロセスでフィールド情報の抽出

アノテーションプロセッサ PropertiesProcessorのprocessメソッドで、対象クラスのフィールド定義を抽出します。

PropertiesProcessorのprocessメソッドに処理を記述します。

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (annotations.isEmpty()) {
            return false;
        }
        roundEnv.getElementsAnnotatedWith(ConfigProperties.class).forEach(this::printFields);
        return true;
    }

    private void printFields(Element element) {
        element.getEnclosedElements().forEach(e -> {
            if (e.getKind() == ElementKind.FIELD) {
                System.out.printf(""" 
                        name: %s
                        type: %s
                        sourceType: %s
                        """, e.getSimpleName(), e.asType(), element.asType()
                );
            }
        });
    }
  • annotations引数が空の場合、処理すべきアノテーション付与の要素がないのでfalseを返します。
  • アノテーション ConfigurePropertiesが付与された要素は、roundEnv引数のgetElementsAnnotatedWithメソッドで取得できます。
    この要素の属性をgetEnclosedElementsメソッドで抽出します。
  • 抽出した属性がフィールドかをgetKindで判定します。
  • フィールドの場合、要素の名前を getSimpleNameメソッドで取得、要素の型(FQCN)をasTypeメソッドで取得します。
  • アノテーション付与された型のFQCNをasTypeメソッドで取得します。


4ヶ月前に更新