Quarkus拡張開発入門!独自アノテーションの作り方とビルド時処理を徹底解説
生徒
「Quarkusで自分専用のアノテーションを作って、特定の処理を自動化したいのですが、普通のJavaのアノテーションと同じ作り方で良いのでしょうか?」
先生
「アノテーション自体の定義はJava標準と同じですが、Quarkus拡張(Extension)として機能させるには、ビルド時(Build Time)にそのアノテーションをどう処理するかを記述する必要があります。」
生徒
「ビルド時処理……。難しそうですね。具体的にどうやってアノテーションを検知して、ロジックを組み込むんですか?」
先生
「QuarkusにはJandexというインデックス機能や、BuildStepという強力な仕組みがあります。初心者の方でも分かりやすく、ステップバイステップで解説していきましょう!」
1. Quarkus拡張におけるアノテーションの役割
Quarkus(クオーカス)は「Supersonic Subatomic Java」と呼ばれる通り、圧倒的な起動速度とメモリ効率を誇るJavaフレームワークです。この性能を実現している最大の理由は、従来のフレームワークが実行時に行っていたリフレクションやクラスパスのスキャンを、ビルド時に前倒しして処理する仕組みにあります。 独自のアノテーションを作成する場合も、この「ビルド時に解析する」という考え方が非常に重要です。開発者が作成したアノテーションをQuarkusがビルド中に見つけ出し、それに基づいてプロキシを生成したり、依存注入の挙動を変えたりすることで、実行時の負荷を最小限に抑えています。
2. 拡張機能プロジェクトの構成と準備
Quarkus拡張を開発するには、通常「runtime」と「deployment」という二つのモジュールに分かれた構造を作成します。アノテーション自体の定義は「runtime」モジュールに配置し、そのアノテーションをスキャンして処理するロジックは「deployment」モジュールに記述します。 まずは、アノテーションを定義するための基本的な依存関係がプロジェクトに含まれているか確認しましょう。Quarkusのプロジェクト作成コマンドやMavenアーキタイプを使用すると、必要な構造が自動的に生成されます。
3. 独自アノテーションを定義する
まずは、Java標準の機能を使ってアノテーションを定義します。ここでは例として、メソッドの実行時間をログに出力するための「@LogExecutionTime」というアノテーションを作成してみましょう。このアノテーション自体は非常にシンプルです。 注意点として、Quarkusでビルド時にスキャン対象とするためには、対象のクラスがJandexインデックスに含まれている必要があります。拡張機能内のクラスは自動的にインデックス化されますが、利用者が作成するアプリ側で使う場合は設定が必要になることもあります。
package org.acme.extension.runtime;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
String value() default "Execution";
}
4. Jandexを使用してアノテーションをスキャンする
次に、deploymentモジュール側でこのアノテーションが付与されたメソッドを見つけ出す処理を書きます。Quarkusは「Jandex」というライブラリを使用して、クラスファイルを直接ロードせずにメタデータを高速に検索します。 BuildStepの中でCombinedIndexBuildItemを受け取ることで、アプリケーション全体のクラス情報を参照できます。ここで特定のアノテーションがどこで使われているかを特定し、次の処理に繋げます。以下のコードは、特定のアノテーションが付与されたメソッドの情報を取得する一例です。
package org.acme.extension.deployment;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.DotName;
import java.util.Collection;
class MyExtensionProcessor {
private static final DotName LOG_ANNOTATION = DotName.createSimple("org.acme.extension.runtime.LogExecutionTime");
@BuildStep
void scanAnnotations(CombinedIndexBuildItem indexBuildItem) {
Collection<AnnotationInstance> annotations = indexBuildItem.getIndex().getAnnotations(LOG_ANNOTATION);
for (AnnotationInstance annotation : annotations) {
System.out.println("発見した対象: " + annotation.target().toString());
}
}
}
5. ビルドステップとレコーダーの連携
アノテーションを見つけただけでは何も起こりません。実行時に何らかの動作をさせるには、「Recorder(レコーダー)」という仕組みを使います。レコーダーは、ビルド時に決定した情報を実行時のバイトコードとして記録する役割を担います。 例えば、アノテーションが付いたメソッドに対してインターセプターを有効にする設定をビルド時に行い、実際のロジックはランタイム側に任せます。これにより、実行時に「どのアノテーションがどこにあるか」を探す手間が省けるのです。
6. インターセプターの実装とアノテーションの紐付け
アノテーションがメソッドに付与された際、実際に動作するロジックを実装します。QuarkusはCDI(Contexts and Dependency Injection)をベースにしているため、標準的なインターセプターの仕組みをそのまま利用できます。 作成したアノテーションをインターセプターバインディングとして定義し、その背後で動作するInterceptorクラスを作成します。以下のサンプルは、実際にメソッドの実行前後に処理を差し込む方法を示しています。
package org.acme.extension.runtime;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@LogExecutionTime
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LogExecutionTimeInterceptor {
@AroundInvoke
Object logInvocation(InvocationContext context) throws Exception {
long start = System.currentTimeMillis();
try {
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
System.out.println(context.getMethod().getName() + " の実行時間: " + duration + "ms");
}
}
}
7. アノテーションを認識させるためのbeans.xmlの設定
Quarkus拡張では、多くの設定が自動化されていますが、作成したインターセプターがCDI Beanとして適切に認識されるようにする必要があります。通常、runtimeモジュールの `META-INF/beans.xml` に設定を記述したり、Jandexインデックスを生成するためのMavenプラグインを適用したりします。 また、拡張機能のdeploymentモジュールにおいて、そのインターセプターが「追加のBean」として登録されるようにビルドステップを記述することも一般的です。これにより、利用者がわざわざ設定ファイルを書かなくても、アノテーションを付けるだけで機能が有効になります。
<!-- runtime/src/main/resources/META-INF/beans.xml -->
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_2_0.xsd"
bean-discovery-mode="annotated">
</beans>
8. 実行時の動作確認とデバッグ方法
独自拡張が完成したら、サンプルアプリケーションで実際にアノテーションを使用してみましょう。QuarkusのDev Mode(mvn quarkus:dev)を起動すると、拡張機能の変更が即座に反映されるため、非常に効率的に開発が進められます。 もしアノテーションが効かない場合は、Jandexインデックスが正しく生成されているか、あるいはBuildStepでスペルミスがないかを確認してください。ログ出力に独自のメッセージを仕込むことで、ビルド時にどのクラスがスキャンされたかを追跡することも可能です。
[INFO] Scanning for projects...
[INFO] --- quarkus:3.15.0:dev (default-cli) @ my-app ---
[INFO] MyExtensionProcessor: 発見した対象: void org.acme.AppResource.hello()
[INFO] Listening for transport dt_socket at address: 5005
[INFO] Quarkus 3.15.0 started in 1.234s.
9. 高度なカスタマイズ:アノテーションにパラメータを持たせる
アノテーションに引数を追加して、動作をカスタマイズすることもできます。例えば、ログレベルを指定できるようにしたり、特定の条件下でのみ実行されるようにフィルターをかけたりすることが可能です。 これらのパラメータ値は、ビルド時にAnnotationInstanceから取得し、レコーダーを通じて実行時のインスタンスに渡されます。この「ビルド時に設定を読み取り、実行時のオーバーヘッドをゼロにする」という流れこそが、Quarkus拡張開発の醍醐味と言えるでしょう。大規模なマイクロサービス開発においては、こうした共通処理の隠蔽化が生産性を大きく向上させます。