独自のエクステンションの作成
Quarkus エクステンションは、コア機能に開発者向けの新しい動作を追加するもので、ビルド時のオーグメンテーション (augmentation) とランタイムコンテナーという 2 つの異なる部分で構成されます。オーグメンテーション部分は、アノテーションや XML デスクリプターの読み込みなど、すべてのメタデータ処理を担当します。このオーグメンテーションフェーズの出力は、関連するランタイムサービスの直接的なインスタンス化を担当する、記録されたバイトコードになります。
これは、メタデータがビルド時に一度だけ処理されることを意味し、起動時間を短縮できるだけでなく、処理に使用されるクラスなどがランタイム JVM にロードされない (あるいは存在すらしない) ため、メモリー使用量も節約できます。
| これは詳細なドキュメントです。導入が必要な場合は 初めてのエクステンションの作成 を、または よくある質問 を参照してください。 |
1. エクステンションの理念
このセクションは現在作成中であり、エクステンションを設計・作成する際の指針となる理念をまとめています。
1.1. エクステンションフレームワークが必要な理由
Quarkus の使命は、使用するライブラリーを含めたアプリケーション全体を、従来の手法よりも大幅に少ないリソースで動作するアーティファクトに変換することです。これらは、GraalVM を使用してネイティブアプリケーションを構築するために使用できます。これを行うには、アプリケーションの完全な「クローズドワールド (閉じた世界)」を分析し、理解する必要があります。完全かつ完全なコンテキストがなければ、達成できる最善のことは、部分的で限定的な汎用サポートにとどまります。Quarkus のエクステンションアプローチを採用することで、Java アプリケーションを Kubernetes やクラウドプラットフォームのようなメモリーフットプリントが制約される環境に適合させることができます。
Quarkus のエクステンションフレームワークは、GraalVM を使用しない場合 (例: HotSpot) でも、リソース利用率を大幅に向上させます。エクステンションが実行するアクションを列挙してみましょう。
-
ビルド時のメタデータの収集とコード生成
-
この部分は GraalVM とは関係なく、Quarkus がフレームワークを「ビルド時に」起動する方法です。
-
エクステンションフレームワークは、メタデータの読み込み、クラスのスキャン、および必要に応じたクラスの生成を容易にします。
-
エクステンションの作業のごく一部は生成されたクラスを介して実行時に実行されますが、作業の大部分はビルド時 (デプロイ時と呼ばれます) に行われます。
-
-
アプリケーションのクローズドワールドビューに基づいて、意見の分かれない適切なデフォルト値を適用します (例:
@Entityのないアプリケーションは Hibernate ORM を起動する必要がない)。 -
エクステンションは Substrate VM のコード置換をホストし、ライブラリーが GraalVM 上で動作できるようにします。
-
ほとんどの変更は、基盤となるライブラリーが GraalVM 上で動作するのを助けるためにアップストリームにプッシュされます。
-
すべての変更をアップストリームにプッシュできるわけではありません。エクステンションは、ライブラリーが動作できるように、コードパッチの一種である Substrate VM の置換をホストします。
-
-
アプリケーションのニーズに基づいたデッドコード削除を支援するため、Substrate VM のコード置換をホストします。
-
これはアプリケーションに依存するため、ライブラリー自体で共有することは困難です。
-
たとえば、Quarkus は特定の接続プールとキャッシュプロバイダーのみが必要であることを認識しているため、Hibernate のコードを最適化します。
-
-
リフレクションが必要なクラスなどのメタデータを GraalVM に送信します。
-
この情報はライブラリー (例: Hibernate) ごとに静的なものではありませんが、フレームワークはセマンティックな知識を持っており、どのクラスがリフレクションを必要とするか (例: @Entity クラス) を把握しています。
-
1.2. ランタイムの作業よりもビルド時の作業を優先
フレームワークに起動時 (ランタイム) に作業させるのではなく、可能な限りビルド時 (エクステンションのデプロイ部分) に作業を行うようにしてください。そこで行われる作業が多いほど、そのエクステンションを使用する Quarkus アプリケーションはより小さくなり、ロードが速くなります。
1.3. 設定の公開方法
Quarkus は最も一般的なユースケースを簡素化します。これは、デフォルト設定が統合対象のライブラリーとは異なる場合があることを意味します。
シンプルな体験を最も簡単にするために、SmallRye Config を介して application.properties で設定を統一してください。ライブラリー固有の設定ファイルは避けるか、少なくともオプションにしてください (例: Hibernate ORM の persistence.xml はオプションです)。
エクステンションは、ライブラリーの使い勝手に焦点を当てるのではなく、Quarkus アプリケーションとして全体的な視点で設定を捉えるべきです。たとえば、データベースアクセスの定義は共有タスクであるため、(たとえば hibernate. プロパティーではなく) quarkus.database.url とその関連プロパティーをエクステンション間で共有します。最も有用な設定オプションは、ライブラリーの本来の名前空間ではなく quarkus.[extension]. として公開されるべきです。あまり一般的ではないプロパティーは、ライブラリーの名前空間に残しておくことができます。
Quarkus が最適化できるクローズドワールドの前提を完全に有効にするには、設定オプションを「ビルド時に決定されるもの」と「ランタイムに上書き可能なもの」として検討するのが最善です。もちろん、ホスト、ポート、パスワードのようなプロパティーはランタイムに上書き可能であるべきです。しかし、キャッシュの有効化や JDBC ドライバーの設定といった多くのプロパティーは、アプリケーションの再構築を要求しても安全です。
1.4. CDI によるコンポーネントの公開
コンポーネントの構成において CDI は中心的なプログラミングモデルであるため、フレームワークやエクステンションは、ユーザーアプリケーションで容易に利用できる Bean としてコンポーネントを公開すべきです。たとえば、Hibernate ORM は EntityManagerFactory と EntityManager の Bean を公開し、接続プールは DataSource の Bean を公開します。エクステンションは、これらの Bean 定義をビルド時に登録しなければなりません。
1.4.1. クラスに基づく Bean
エクステンションは AdditionalBeanBuildItem を生成して、あたかも元のアプリケーションの一部であるかのようにクラスから Bean 定義を読み込むようコンテナーに指示できます。
AdditionalBeanBuildItem によって登録された Bean クラス@Singleton (1)
public class Echo {
public String echo(String val) {
return val;
}
}
| 1 | AdditionalBeanBuildItem によって登録された Bean がスコープを指定していない場合は、@Dependent であると見なされます。 |
他のすべての Bean は、そのような Bean をインジェクトできます。
AdditionalBeanBuildItem によって生成された Bean をインジェクトする Bean@Path("/hello")
public class ExampleResource {
@Inject
Echo echo;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello(String foo) {
return echo.echo(foo);
}
}
その逆も同様で、エクステンションの Bean はアプリケーションの Bean や他のエクステンションが提供する Bean をインジェクトできます。
@Singleton
public class Echo {
@Inject
DataSource dataSource; (1)
@Inject
Instance<List<String>> listsOfStrings; (2)
//...
}
| 1 | 他のエクステンションが提供する Bean をインジェクトします。 |
| 2 | List<String> 型に一致するすべての Bean をインジェクトします。 |
1.4.2. Bean の初期化
一部のコンポーネントは、オーグメンテーション中に収集された情報に基づいて追加の初期化が必要な場合があります。最も単純な解決策は、Bean インスタンスを取得してビルドステップから直接メソッドを呼び出すことです。しかし、オーグメンテーションフェーズ中に Bean インスタンスを取得することは illegal です。理由は、CDI コンテナーがまだ起動していないためです。CDI コンテナーは Static init bootstrap フェーズ 中に起動されます。
BUILD_AND_RUN_TIME_FIXED および RUN_TIME 設定ルートは、任意の Bean にインジェクトできます。ただし、RUN_TIME 設定ルートのインジェクトはブートストラップ後に行うべきです。
|
ただし、レコーダーメソッド (recorder method) から Bean メソッドを呼び出すことは可能です。@Record(STATIC_INIT) ビルドステップで Bean にアクセスする必要がある場合は、BeanContainerBuildItem に依存するか、ロジックを BeanContainerListenerBuildItem でラップする必要があります。理由は単純で、CDI コンテナーが完全に初期化され、起動していることを確認する必要があるからです。一方、@Record(RUNTIME_INIT) ビルドステップでは、CDI コンテナーが完全に初期化されて実行中であると期待しても安全です。コンテナーへの参照は、CDI.current() または Quarkus 固有の Arc.container() を介して取得できます。
Bean の状態が (たとえば volatile キーワードなどによって) 可視性を保証するようにすることを忘れないでください。
|
| この「遅延初期化」アプローチには、1 つの大きな欠点があります。 uninitialized な Bean が、ブートストラップ中にインスタンス化される他のエクステンションやアプリケーションコンポーネントからアクセスされる可能性があることです。より堅牢な解決策については、 合成 Bean (Synthetic Bean) で説明します。 |
1.4.3. デフォルト Bean
このような Bean を作成しつつ、アプリケーションコードでカスタム実装を使用して一部の Bean を簡単にオーバーライドできるようにするための非常に便利なパターンは、Quarkus が提供する @DefaultBean を使用することです。これは例で説明するのが最も分かりやすいでしょう。
Quarkus エクステンションが、アプリケーションコードが独自の Bean にインジェクトすることを想定した Tracer Bean を提供する必要があると仮定します。
@Dependent
public class TracerConfiguration {
@Produces
public Tracer tracer(Reporter reporter, Configuration configuration) {
return new Tracer(reporter, configuration);
}
@Produces
@DefaultBean
public Configuration configuration() {
// create a Configuration
}
@Produces
@DefaultBean
public Reporter reporter(){
// create a Reporter
}
}
たとえば、アプリケーションコードが Tracer を使用したいが、カスタムの Reporter Bean も使用する必要がある場合、そのような要件は次のようにして簡単に実現できます。
@Dependent
public class CustomTracerConfiguration {
@Produces
public Reporter reporter(){
// create a custom Reporter
}
}
1.4.4. @DefaultBean を使用しないライブラリー/Quarkus エクステンションで定義された Bean をオーバーライドする方法
@DefaultBean が推奨される方法ですが、Bean を CDI の @Alternative としてマークし、@Priority アノテーションを含めることで、アプリケーションコードがエクステンションによって提供される Bean をオーバーライドすることも可能です。簡単な例を見てみましょう。架空の "quarkus-parser" エクステンションに取り組んでおり、デフォルトの Bean 実装があるとします。
@Dependent
class Parser {
String[] parse(String expression) {
return expression.split("::");
}
}
そして、私たちのエクステンションもこのパーサーを利用します。
@ApplicationScoped
class ParserService {
@Inject
Parser parser;
//...
}
ここで、ユーザーや他のエクステンションが Parser のデフォルト実装をオーバーライドする必要がある場合、最も単純な解決策は CDI の @Alternative + @Priority を使用することです。
@Alternative (1)
@Priority(1) (2)
@Singleton
class MyParser extends Parser {
String[] parse(String expression) {
// my super impl...
}
}
| 1 | MyParser は代替 Bean です。 |
| 2 | 代替を有効にします。優先度はデフォルト Bean をオーバーライドする任意の数値にできますが、複数の代替がある場合は最も高い優先度が優先されます。 |
| CDI 代替案は、インジェクションと型安全な解決の際のみ考慮されます。たとえば、デフォルトの実装であっても、オブザーバー通知は引き続き受信します。 |
1.4.5. 合成 Bean (Synthetic Bean)
合成 Bean を登録できると非常に便利な場合があります。合成 Bean の Bean 属性は、Java のクラス、メソッド、またはフィールドから派生したものではありません。代わりに、属性はエクステンションによって指定されます。
| CDI コンテナーは合成 Bean のインスタンス化を制御しないため、依存性注入やその他のサービス (インターセプターなど) はサポートされません。言い換えれば、合成 Bean インスタンスに必要なすべてのサービスを提供するのはエクステンション側の役割です。 |
Quarkus で 合成 Bean を登録する方法はいくつかあります。この章では、(Bean の初期化 と比較して) 安全な方法でエクステンションの Bean を初期化するために使用できるユースケースを取り上げます。
SyntheticBeanBuildItem を使用して、合成 Bean を登録できます。
-
そのインスタンスは、レコーダー を通じて簡単に生成できます。
-
オーグメンテーション中に収集されたすべての情報を保持する「コンテキスト」Bean を提供することで、実際のコンポーネントがコンテキスト Bean を直接インジェクトできるため、「遅延初期化」を必要としなくなります。
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(Foo.class).scope(Singleton.class)
.runtimeValue(recorder.createFoo("parameters are recorded in the bytecode")) (1)
.done();
}
| 1 | 文字列の値がバイトコードに記録され、Foo のインスタンスの初期化に使用されます。 |
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(TestContext.class).scope(Singleton.class)
.runtimeValue(recorder.createContext("parameters are recorded in the bytecode")) (1)
.done();
}
| 1 | 「実際の」コンポーネントは TestContext を直接インジェクトできます。 |
1.5. エクステンションのいくつかの種類
エクステンションには複数のステレオタイプが存在します。いくつか挙げてみましょう。
- 素のライブラリーの実行
-
これは、あまり高度ではないエクステンションです。ライブラリーが GraalVM 上で動作するようにするためのパッチのセットで構成されます。可能であれば、これらのパッチはエクステンションではなく、アップストリームに寄贈してください。次善の策は、ネイティブイメージのコンパイル中に適用されるパッチである Substrate VM の置換を書くことです。
- フレームワークの実行
-
ランタイムのフレームワークは通常、設定の読み込み、クラスパスやクラスのメタデータ (アノテーション、getter など) のスキャン、動作のベースとなるメタモデルの構築、サービスローダーパターンによるオプションの検索、呼び出しの準備 (リフレクション)、プロキシインターフェースなどを行います。これらの操作はビルド時に行い、メタモデルをレコーダー DSL に渡す必要があります。これにより、ランタイムに実行されフレームワークを起動するクラスが生成されます。
- CDI ポータブルエクステンションの実行
-
CDI ポータブルエクステンションのモデルは非常に柔軟です。柔軟すぎて、Quarkus が推進するビルド時の起動というメリットを享受できません。私たちが見てきたほとんどのエクステンションは、このような極端な柔軟性の機能を利用していません。CDI エクステンションを Quarkus に移植する方法は、ビルド時 (エクステンションの用語ではデプロイ時) にさまざまな Bean を定義する Quarkus エクステンションとして書き直すことです。
1.6. 機能のレベル
Quarkus エクステンションは多くのことができます。 エクステンション成熟度マトリックス は、推奨される実装順序とともに、さまざまな機能を通じたパスを示しています。
2. 技術的側面
2.1. ブートストラップの 3 つのフェーズと Quarkus の理念
Quarkus アプリには 3 つの異なるブートストラップフェーズがあります。
- オーグメンテーション (Augmentation)
-
これは最初のフェーズで、 ビルドステッププロセッサー によって行われます。これらのプロセッサーは Jandex のアノテーション情報にアクセスでき、任意のデスクリプターを解析してアノテーションを読み取ることができますが、アプリケーションクラスをロードしようとしてはいけません。これらのビルドステップの出力は、 Gizmo というバイトコード生成ライブラリーを使用して生成された、記録されたバイトコードであり、ランタイムにアプリケーションを実際にブートストラップするために使用されます。ビルドステップに関連付けられた
@io.quarkus.deployment.annotations.Recordアノテーションのio.quarkus.deployment.annotations.ExecutionTime値に応じて、ステップは次の 2 つのモードに基づいて異なる JVM で実行される場合があります。 - Static Init
-
@Record(STATIC_INIT)でバイトコードが記録された場合、メインクラスの static init メソッドから実行されます。ネイティブ実行可能ファイルのビルドでは、このコードはネイティブビルドプロセスの一部として通常の JVM で実行され、このステージで生成され保持されたオブジェクトは、イメージマップされたファイルを介してネイティブ実行可能ファイルに直接シリアライズされます。これは、フレームワークがこのフェーズで起動できれば、その起動状態がイメージに直接書き込まれるため、イメージの起動時に起動コードを実行する必要がないことを意味します。Substrate VM がネイティブ実行可能ファイル内の一部のオブジェクトを許可していないため、このステージで実行できることにはいくつかの制限があります。たとえば、このフェーズでポートをリッスンしたり、スレッドを開始したりしようとしてはいけません。また、static 初期化中にランタイム設定を読み取ることは禁止されています。
ネイティブではない純粋な JVM モードでは、Static Init が常に最初に実行されるという点を除き、Static Init と Runtime Init に実質的な違いはありません。このモードでも、デスクリプターの解析やアノテーションのスキャンがビルド時に行われ、関連するクラス/フレームワークの依存関係をビルド出力の jar から削除できるため、ネイティブモードと同じビルドフェーズでのオーグメンテーションの恩恵を受けられます。WildFly のようなサーバーでは、XML パーサーなどのデプロイ関連のクラスがアプリケーションの有効期間中残り続け、貴重なメモリーを消費します。Quarkus はこれを排除し、ランタイムにロードされるクラスが、実際にランタイムで使用されるものだけになるようにすることを目指しています。
例として、Quarkus アプリケーションが XML パーサーをロードする唯一の理由は、ユーザーがアプリケーション内で XML を使用している場合であるべきです。設定の XML 解析は、すべてオーグメンテーションフェーズで行われるべきです。
- Runtime Init
-
@Record(RUNTIME_INIT)でバイトコードが記録された場合、アプリケーションのメインメソッドから実行されます。このコードはネイティブ実行可能ファイルの起動時に実行されます。一般的に、このフェーズで実行されるコードは可能な限り少なくし、ポートを開く必要があるコードなどに限定すべきです。
可能な限り多くのことを @Record(STATIC_INIT) フェーズに押し出すことで、2 つの異なる最適化が可能になります。
-
ネイティブ実行可能モードと純粋な JVM モードの両方において、処理がビルド中に行われているため、アプリケーションを可能な限り速く起動できます。また、アプリケーションに必要なクラスやネイティブコードを、純粋にランタイムに関連する動作のみに最小化できます。
-
ネイティブ実行可能モードにおけるもう 1 つの利点は、Substrate が使用されていない機能をより簡単に排除できることです。機能がバイトコードを介して直接初期化される場合、Substrate はメソッドが呼び出されないことを検出して、そのメソッドを削除できます。設定がランタイムに読み取られる場合、Substrate は設定の内容を推論できないため、必要になる場合に備えてすべての機能を保持しておく必要があります。
2.2. プロジェクトのセットアップ
エクステンションプロジェクトは、2 つのサブモジュールを持つマルチモジュールプロジェクトとして構成する必要があります。
-
ビルド時の処理とバイトコードの記録を処理するデプロイ時サブモジュール。
-
ネイティブ実行可能ファイルまたはランタイム JVM でエクステンションの動作を提供する、ランタイムサブモジュール。
ランタイムアーティファクトは io.quarkus:quarkus-core に依存する必要があり、他の Quarkus モジュールが提供する機能を使用したい場合は、それらのランタイムアーティファクトにも依存する可能性があります。
デプロイ時モジュールは、io.quarkus:quarkus-core-deployment、独自のランタイムアーティファクト、およびエクステンションが依存する他の Quarkus エクステンションのデプロイアーティファクトに依存する必要があります。これは不可欠であり、そうでないと、推移的に取り込まれたエクステンションがその全機能を提供できなくなります。
| Maven および Gradle プラグインはこれを自動的に検証し、追加し忘れている可能性のあるデプロイアーティファクトについて警告します。 |
|
いかなる状況においても、ランタイムモジュールがデプロイアーティファクトに依存することはできません。これを行うと、すべてのデプロイ時コードがランタイムスコープに引き込まれてしまい、モジュールを分割した目的が損なわれてしまいます。 |
2.2.1. Maven の使用
io.quarkus:quarkus-extension-maven-plugin を含め、エクステンションアーティファクトに必要な Quarkus エクステンションメタデータ を収集・生成するために quarkus-extension-processor アノテーションプロセッサーを検出するように maven-compiler-plugin を設定する必要があります。Quarkus の親 POM を使用している場合は、正しい設定が自動的に継承されます。
これらの Maven モジュールを作成するには、io.quarkus.platform:quarkus-maven-plugin の create-extension mojo を使用するとよいでしょう。詳細は次のセクションを参照してください。
|
慣習として、デプロイ時アーティファクトには -deployment サフィックスを付け、ランタイムアーティファクトにはサフィックスを付けません (エンドユーザーがプロジェクトに追加するのはこちらです)。
|
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-maven-plugin</artifactId>
<!-- Executions configuration can be inherited from quarkus-build-parent -->
<executions>
<execution>
<goals>
<goal>extension-descriptor</goal>
</goals>
<configuration>
<deployment>${project.groupId}:${project.artifactId}-deployment:${project.version}</deployment>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
上記の maven-compiler-plugin 設定にはバージョン 3.5 以上が必要です。
|
また、quarkus-extension-processor アノテーションプロセッサーを検出するように、デプロイモジュールの maven-compiler-plugin を設定する必要もあります。
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2.2.1.1. Maven を使用した新しい Quarkus Core エクステンションモジュールの作成
Quarkus は、エクステンションプロジェクトを初期化するための create-extension Maven Mojo を提供しています。
以下のオプションを自動検出しようとします。
-
quarkus(Quarkus Core) またはquarkus/extensionsディレクトリ内であれば、「Quarkus Core」エクステンションのレイアウトとデフォルト値を使用します。 -
-DgroupId=io.quarkiverse.[extensionId]が指定されている場合は、「Quarkiverse」エクステンションのレイアウトとデフォルト値を使用します。 -
その他の場合は、「スタンドアロン」エクステンションのレイアウトとデフォルト値を使用します。
-
将来的に他のレイアウトタイプを導入する可能性があります。
対話モードを使用するためにパラメーターを指定しないこともできます。 mvn io.quarkus.platform:quarkus-maven-plugin:3.35.2:create-extension -N
|
例として、Quarkus のソースツリーに my-ext という新しいエクステンションを追加してみましょう。
git clone https://github.com/quarkusio/quarkus.git
cd quarkus
mvn io.quarkus.platform:quarkus-maven-plugin:3.35.2:create-extension -N \
-DextensionId=my-ext \
-DextensionName="My Extension" \
-DextensionDescription="Do something useful."
デフォルトでは、groupId、version、quarkusVersion、namespaceId、および namespaceName は、他の Quarkus コアエクステンションと一致するように設定されます。
|
| エクステンションの説明は、 https://code.quarkus.io/ 上や、Quarkus CLI でエクステンションを一覧表示する際などに表示されるため重要です。 |
上記の一連のコマンドは以下を実行します。
-
4 つの新しい Maven モジュールを作成します。
-
extensions/my-extディレクトリ内のquarkus-my-ext-parent -
extensions/my-ext/runtimeディレクトリ内のquarkus-my-ext -
extensions/my-ext/deploymentディレクトリ内のquarkus-my-ext-deployment。このモジュールには基本的なMyExtProcessorクラスが生成されます。 -
integration-tests/my-ext/deploymentディレクトリ内のquarkus-my-ext-integration-test。このモジュールには、空の Jakarta REST リソースクラスと 2 つのテストクラス (JVM モード用とネイティブモード用) が生成されます。
-
-
必要に応じてこれら 3 つのモジュールをリンクします。
-
quarkus-my-ext-parentがquarkus-extensions-parentの<modules>に追加されます。 -
quarkus-my-extが Quarkus BOM (Bill of Materials) であるbom/application/pom.xmlの<dependencyManagement>に追加されます。 -
quarkus-my-ext-deploymentが Quarkus BOM (Bill of Materials) であるbom/application/pom.xmlの<dependencyManagement>に追加されます。 -
quarkus-my-ext-integration-testがquarkus-integration-tests-parentの<modules>に追加されます。
-
また、ランタイムモジュールの src/main/resources/META-INF フォルダー内にある、エクステンションを記述する quarkus-extension.yaml テンプレートファイルの内容を埋める必要があります。
|
これは quarkus-agroal エクステンションの quarkus-extension.yaml テンプレートです。例として使用できます。
name: "Agroal - Database connection pool" (1)
metadata:
keywords: (2)
- "agroal"
- "database-connection-pool"
- "datasource"
- "jdbc"
guide: "https://quarkus.io/guides/datasource" (3)
categories: (4)
- "data"
status: "stable" (5)
| 1 | ユーザーに表示されるエクステンションの名前 |
| 2 | エクステンションカタログでエクステンションを検索するために使用できるキーワード |
| 3 | エクステンションのガイドまたはドキュメントへのリンク |
| 4 | code.quarkus.io 上でエクステンションが表示されるカテゴリー。省略可能ですが、その場合、エクステンションは一覧表示されますが、特定のカテゴリーには分類されません。 |
| 5 | 成熟度ステータス。エクステンションのメンテナーによって評価された stable、preview、または experimental のいずれかを指定します。 |
mojo の name パラメーターはオプションです。コマンドラインで指定しない場合、プラグインは extensionId から、ダッシュをスペースに置き換え、各トークンを大文字にすることで名前を導出します。そのため、明示的な name の指定を省略できる場合もあります。
|
mojo のすべての利用可能なオプションについては、 CreateExtensionMojo JavaDoc を参照してください。
2.2.2. Gradle の使用
エクステンションプロジェクトの runtime モジュールに io.quarkus.extension プラグインを適用する必要があります。このプラグインには、META-INF/quarkus-extension.properties および META-INF/quarkus-extension.yml ファイルを生成する extensionDescriptor タスクが含まれています。また、このプラグインは、残りの Quarkus エクステンションメタデータ を収集・生成するために、deployment と runtime の両方のモジュールで io.quarkus:quarkus-extension-processor アノテーションプロセッサーを有効にします。デプロイモジュールの名前は、プラグインで deploymentModule プロパティーを設定することで変更できます。デフォルトでは、このプロパティーは deployment に設定されています。
plugins {
id 'java'
id 'io.quarkus.extension'
}
quarkusExtension {
deploymentModule = 'deployment'
}
dependencies {
implementation platform('io.quarkus:quarkus-bom:3.35.2')
}
2.3. ビルドステッププロセッサー
オーグメンテーション時の作業は、 build items を生成・消費する build steps によって行われます。プロジェクトビルド内のエクステンションに対応するデプロイモジュールで見つかったビルドステップは、自動的に相互に接続されて実行され、最終的なビルドアーティファクトを生成します。
2.3.1. ビルドステップ
build step は、 @io.quarkus.deployment.annotations.BuildStep アノテーションが付与された非 static メソッドです。
各ビルドステップは、前のステージで生成されたアイテムを 消費 し、後のステージで消費できるアイテムを 生成 できます。ビルドステップは通常、最終的に別のステップによって消費されるビルドアイテムを生成する場合にのみ実行されます。
ビルドステップは通常、エクステンションのデプロイモジュール内のプレーンなクラスに配置されます。これらのクラスはオーグメントプロセス中に自動的にインスタンス化され、インジェクション を利用します。
2.3.2. ビルドアイテム
ビルドアイテムは、抽象クラス io.quarkus.builder.item.BuildItem の具体的な final サブクラスです。各ビルドアイテムは、あるステージから別のステージへ渡す必要のある何らかの情報単位を表します。基底の BuildItem クラスを直接サブクラス化することはできません。代わりに、作成 may なビルドアイテムの種類ごとに抽象サブクラスが用意されています。 simple 、 multi 、 empty 。
ビルドアイテムは、異なるエクステンション同士が通信するための手段と考えてください。たとえば、ビルドアイテムは以下のようなことができます。
-
データベース設定が存在するという事実を公開する。
-
そのデータベース設定を消費する (例: 接続プールエクステンションや ORM エクステンション)。
-
あるエクステンションが別のエクステンションに作業を依頼する。例: 新しい CDI Bean を定義したいエクステンションが、ArC エクステンションにその定義を依頼する。
これは非常に柔軟なメカニズムです。
生成者/消費者モデルでは変更の順序付けを正しく行えないため、BuildItem インスタンスは不変 (immutable) であるべきです。これは強制されていませんが、このルールに従わないとレースコンディションが発生する可能性があります。
|
ビルドステップは、他のビルドステップによって (推移的に) 必要とされるビルドアイテムを生成する場合にのみ実行されます。ビルドステップがビルドアイテムを生成することを確認してください。そうでない場合は、ビルド検証のための ValidationErrorBuildItem か、生成されたアーティファクトのための ArtifactResultBuildItem のいずれかを生成すべきでしょう。
|
2.3.2.1. Simple ビルドアイテム
Simple ビルドアイテムは、io.quarkus.builder.item.SimpleBuildItem を継承する final クラスです。Simple ビルドアイテムは、特定のビルドにおいて 1 つのステップによってのみ生成されます。1 つのビルドで複数のステップが同じ Simple ビルドアイテムを生成すると宣言した場合、エラーが発生します。Simple ビルドアイテムは、任意の数のビルドステップで消費できます。Simple ビルドアイテムを消費するビルドステップは、そのアイテムを生成したビルドステップよりも after に常に実行されます。
/**
* The build item which represents the Jandex index of the application,
* and would normally be used by many build steps to find usages
* of annotations.
*/
public final class ApplicationIndexBuildItem extends SimpleBuildItem {
private final Index index;
public ApplicationIndexBuildItem(Index index) {
this.index = index;
}
public Index getIndex() {
return index;
}
}
2.3.2.2. Multi ビルドアイテム
Multiple または「multi」ビルドアイテムは、io.quarkus.builder.item.MultiBuildItem を継承する final クラスです。特定のクラスの Multi ビルドアイテムは、任意の数のステップによって任意の数だけ生成できますが、Multi ビルドアイテムを消費するステップは、それらを生成できるすべてのステップが完了した after にのみ実行されます。
public final class ServiceWriterBuildItem extends MultiBuildItem {
private final String serviceName;
private final List<String> implementations;
public ServiceWriterBuildItem(String serviceName, String... implementations) {
this.serviceName = serviceName;
// Make sure it's immutable
this.implementations = Collections.unmodifiableList(
Arrays.asList(
implementations.clone()
)
);
}
public String getServiceName() {
return serviceName;
}
public List<String> getImplementations() {
return implementations;
}
}
/**
* This build step produces a single multi build item that declares two
* providers of one configuration-related service.
*/
@BuildStep
public ServiceWriterBuildItem registerOneService() {
return new ServiceWriterBuildItem(
Converter.class.getName(),
MyFirstConfigConverterImpl.class.getName(),
MySecondConfigConverterImpl.class.getName()
);
}
/**
* This build step produces several multi build items that declare multiple
* providers of multiple configuration-related services.
*/
@BuildStep
public void registerSeveralServices(
BuildProducer<ServiceWriterBuildItem> providerProducer
) {
providerProducer.produce(new ServiceWriterBuildItem(
Converter.class.getName(),
MyThirdConfigConverterImpl.class.getName(),
MyFourthConfigConverterImpl.class.getName()
));
providerProducer.produce(new ServiceWriterBuildItem(
ConfigSource.class.getName(),
MyConfigSourceImpl.class.getName()
));
}
/**
* This build step aggregates all the produced service providers
* and outputs them as resources.
*/
@BuildStep
public void produceServiceFiles(
List<ServiceWriterBuildItem> items,
BuildProducer<GeneratedResourceBuildItem> resourceProducer
) throws IOException {
// Aggregate all the providers
Map<String, Set<String>> map = new HashMap<>();
for (ServiceWriterBuildItem item : items) {
String serviceName = item.getName();
for (String implName : item.getImplementations()) {
map.computeIfAbsent(
serviceName,
(k, v) -> new LinkedHashSet<>()
).add(implName);
}
}
// Now produce the resource(s) for the SPI files
for (Map.Entry<String, Set<String>> entry : map.entrySet()) {
String serviceName = entry.getKey();
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
try (OutputStreamWriter w = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
for (String implName : entry.getValue()) {
w.write(implName);
w.write(System.lineSeparator());
}
w.flush();
}
resourceProducer.produce(
new GeneratedResourceBuildItem(
"META-INF/services/" + serviceName,
os.toByteArray()
)
);
}
}
}
2.3.2.3. Empty ビルドアイテム
Empty ビルドアイテムは、io.quarkus.builder.item.EmptyBuildItem を継承する final クラス (通常は空のクラス) です。これらは、実際にはデータを保持しないビルドアイテムを表し、空のクラスをインスタンス化することなく、そのようなアイテムの生成と消費を可能にします。これら自体をインスタンス化することはできません。
これらはインスタンス化できないため、いかなる手段によってもインジェクトできず、ビルドステップから (または BuildProducer を介して) 返すこともできません。Empty ビルドアイテムを生成するには、ビルドステップに @Produce(MyEmptyBuildItem.class) を付与し、消費するには @Consume(MyEmptyBuildItem.class) を付与する必要があります。
|
public final class NativeImageBuildItem extends EmptyBuildItem {
// empty
}
Empty ビルドアイテムは、ステップ間の順序を強制する「バリア」として機能します。また、一般的なビルドシステムが「疑似ターゲット (pseudo-target)」を使用するのと同じように使用することもできます。つまり、ビルドアイテムが具体的な表現を持たない概念的な目標を表すことができます。
/**
* Contrived build step that produces the native image on disk. The main augmentation
* step (which is run by Maven or Gradle) would be declared to consume this empty item,
* causing this step to be run.
*/
@BuildStep
@Produce(NativeImageBuildItem.class)
void produceNativeImage() {
// ...
// (produce the native image)
// ...
}
/**
* This would always run after {@link #produceNativeImage()} completes, producing
* an instance of {@code SomeOtherBuildItem}.
*/
@BuildStep
@Consume(NativeImageBuildItem.class)
SomeOtherBuildItem secondBuildStep() {
return new SomeOtherBuildItem("foobar");
}
2.3.2.4. Validation Error ビルドアイテム
これらは、ビルドを失敗させる検証エラーを伴うビルドアイテムを表します。これらのビルドアイテムは、CDI コンテナーの初期化中に消費されます。
@BuildStep
void checkCompatibility(Capabilities capabilities, BuildProducer<ValidationErrorBuildItem> validationErrors) {
if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)
&& capabilities.isPresent(Capability.RESTEASY)) {
validationErrors.produce(new ValidationErrorBuildItem(
new ConfigurationException("Cannot use both RESTEasy Classic and Reactive extensions at the same time")));
}
}
2.3.3. インジェクション
ビルドステップを含むクラスは、以下の種類のインジェクションをサポートしています。
-
コンストラクターパラメーターインジェクション
-
フィールドインジェクション
-
メソッドパラメーターインジェクション (ビルドステップメソッドのみ)
ビルドステップクラスは、ビルドステップの呼び出しごとにインスタンス化およびインジェクトされ、その後破棄されます。ステップが同じクラス上にある場合でも、ビルドステップ間の状態の受け渡しはビルドアイテムを介してのみ行う必要があります。
| final フィールドはインジェクションの対象になりませんが、必要に応じてコンストラクターパラメーターインジェクションを介して値を設定できます。static フィールドがインジェクションの対象になることはありません。 |
インジェクト可能な値の種類には以下のものが含まれます。
-
前のビルドステップによって生成された ビルドアイテム
-
後続のビルドステップのためのアイテムを生成する ビルドプロデューサー
-
設定マッピング 型
-
バイトコード記録 用のテンプレートオブジェクト
| ビルドステップメソッドまたはそのクラスにインジェクトされたオブジェクトを、そのメソッドの実行以外で使用しては must not 。 |
| インジェクションはアノテーションプロセッサーを介してコンパイル時に解決され、生成されたコードには private フィールドへのインジェクトや private メソッドの呼び出し権限はありません。 |
2.3.4. 値の生成
ビルドステップは、いくつかの可能な方法で後続のステップのための値を生成できます。
-
Simple ビルドアイテム または Multi ビルドアイテム のインスタンスを返す。
-
Multi ビルドアイテムクラスの
Listを返す。 -
Simple または Multi ビルドアイテムクラスの
BuildProducerをインジェクトする。 -
メソッドに
@io.quarkus.deployment.annotations.Produceを付与し、Empty ビルドアイテム のクラス名を指定する。
Simple ビルドアイテムがビルドステップで宣言されている場合、そのビルドステップ中に must 生成される必要があり、そうでない場合はエラーになります。ステップにインジェクトされたビルドプロデューサーを、そのステップの外で使用しては must not 。
@BuildStep メソッドは、他の消費者または最終出力が必要とするものを生成する場合にのみ呼び出されることに注意してください。特定のアイテムに消費者が存在しない場合、そのアイテムは生成されません。何が必要かは、生成される最終ターゲットによって異なります。たとえば、開発モードで実行する場合、最終出力は ReflectiveClassBuildItem のような GraalVM 固有のビルドアイテムを要求しないため、これらのアイテムのみを生成するメソッドは呼び出されません。
2.3.5. 値の消費
ビルドステップは、以下の方法で前のステップの値を利用できます。
-
シンプルビルドアイテム をインジェクトする
-
シンプルビルドアイテムクラスの
Optionalをインジェクトする -
マルチビルドアイテム クラスの
Listをインジェクトする -
メソッドに
@io.quarkus.deployment.annotations.Consumeアノテーションを付与し、 空のビルドアイテム のクラス名を指定する
通常、他のどのステップからも生成されないシンプルビルドアイテムを利用するステップが含まれている場合は、エラーになります。これにより、ステップの実行時に、宣言されたすべての値が存在し、 null でないことが保証されます。
ビルドを完了させるために必ずしも必要ではないが、存在すればビルドステップの動作に影響を与える値が必要な場合があります。この場合、値はオプションとしてインジェクトできます。
| マルチビルド値は常に オプション と見なされます。存在しない場合は、空のリストがインジェクトされます。 |
2.3.5.1. 弱い値の生成
通常、ビルドステップは、他のビルドステップによって消費されるビルドアイテムを生成する場合には常に含まれます。このようにして、最終的なアーティファクトの生成に必要なステップのみが含まれ、インストールされていないエクステンションに関するステップや、特定のアーティファクトタイプに関係のないビルドアイテムのみを生成するステップは除外されます。
このような動作を望まない場合は、 @io.quarkus.deployment.annotations.Weak アノテーションを使用できます。このアノテーションは、アノテーションが付与された値を生成するという理由だけで、ビルドステップが自動的に含まれるべきではないことを示します。
/**
* This build step is only run if something consumes the ExecutorClassBuildItem.
*/
@BuildStep
void createExecutor(
@Weak BuildProducer<GeneratedClassBuildItem> classConsumer,
BuildProducer<ExecutorClassBuildItem> executorClassConsumer
) {
ClassWriter cw = new ClassWriter(Gizmo.ASM_API_VERSION);
String className = generateClassThatCreatesExecutor(cw); (1)
classConsumer.produce(new GeneratedClassBuildItem(true, className, cw.toByteArray()));
executorClassConsumer.produce(new ExecutorClassBuildItem(className));
}
| 1 | このメソッド (この例では提供されていません) は、 ASM API を使用してクラスを生成します。 |
生成されたクラスやリソースなど、特定の種類のビルドアイテムは、通常常に消費されます。エクステンションは、そのビルドアイテムの使用を容易にするために、生成されたクラスとともにビルドアイテムを生成することがあります。このようなビルドステップでは、生成されたクラスのビルドアイテムに @Weak アノテーションを使用し、もう一方のビルドアイテムは通常通り生成します。もう一方のビルドアイテムが最終的に何らかの形で消費されれば、ステップが実行され、クラスが生成されます。何もそのビルドアイテムを消費しない場合、そのステップはビルドプロセスに含まれません。
上記の例では、 GeneratedClassBuildItem は、 ExecutorClassBuildItem が他のビルドステップによって消費された場合にのみ生成されます。
バイトコードレコーディング を使用する場合、 @io.quarkus.deployment.annotations.Record アノテーションの optional 属性を使用することで、暗黙的に生成されるクラスを弱く宣言できることに注意してください。
/**
* This build step is only run if something consumes the ExecutorBuildItem.
*/
@BuildStep
@Record(value = ExecutionTime.RUNTIME_INIT, optional = true) (1)
ExecutorBuildItem createExecutor( (2)
ExecutorRecorder recorder,
ThreadPoolConfig threadPoolConfig
) {
return new ExecutorBuildItem(
recorder.setupRunTime(
shutdownContextBuildItem,
threadPoolConfig,
launchModeBuildItem.getLaunchMode()
)
);
}
| 1 | optional 属性に注目してください。 |
| 2 | この例ではレコーダープロキシを使用しています。詳細については、 バイトコードレコーディング のセクションを参照してください。 |
2.3.6. アプリケーションアーカイブ
@BuildStep アノテーションは、クラスパス上のどのアーカイブが「アプリケーションアーカイブ」と見なされるかを決定するマーカーファイルを登録することもでき、それによってインデックスが作成されます。これは applicationArchiveMarkers を介して行われます。例えば、 ArC エクステンションは META-INF/beans.xml を登録します。これは、クラスパス上の beans.xml ファイルを持つすべてのアーカイブがインデックス化されることを意味します。
2.3.7. スレッドのコンテキストクラスローダーの使用
ビルドステップは、トランスフォーマーセーフな方法でデプロイメントからユーザークラスをロードできる TCCL を使用して実行されます。このクラスローダーはオーグメンテーションの間だけ持続し、その後破棄されます。クラスは実行時に別のクラスローダーで再度ロードされます。つまり、オーグメンテーション中にクラスをロードしても、開発/テストモードでの実行時にそのクラスが変換されるのを妨げることはありません。
2.3.8. IndexDependencyBuildItem による外部 JAR のインデクサーへの追加
スキャンされたクラスのインデックスには、外部クラスの依存関係が自動的には含まれません。依存関係を追加するには、 groupId と artifactId に対して IndexDependencyBuildItem オブジェクトを生成する @BuildStep を作成します。
| インデクサーに追加する必要なアーティファクトをすべて指定することが重要です。推移的に追加されるアーティファクトはありません。 |
Amazon Alexa エクステンションは、 Jackson の JSON 変換で使用される Alexa SDK の依存ライブラリーを追加し、リフレクティブなクラスが識別され、 BUILD_TIME に含められるようにします。
@BuildStep
void addDependencies(BuildProducer<IndexDependencyBuildItem> indexDependency) {
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-runtime"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-model"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-lambda-support"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-servlet-support"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-dynamodb-persistence-adapter"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-apache-client"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-model-runtime"));
}
Jandex インデクサーにアーティファクトを追加すると、インデックスを検索して、インターフェースを実装しているクラス、特定のクラスのサブクラス、またはターゲットアノテーションを持つクラスを識別できるようになります。
例えば、 Jackson エクステンションは、以下のようなコードを使用して JSON デシリアライズで使用されるアノテーションを検索し、 BUILD_TIME 分析のためにそれらをリフレクティブ階層に追加します。
DotName JSON_DESERIALIZE = DotName.createSimple(JsonDeserialize.class.getName());
IndexView index = combinedIndexBuildItem.getIndex();
// handle the various @JsonDeserialize cases
for (AnnotationInstance deserializeInstance : index.getAnnotations(JSON_DESERIALIZE)) {
AnnotationTarget annotationTarget = deserializeInstance.target();
if (CLASS.equals(annotationTarget.kind())) {
DotName dotName = annotationTarget.asClass().name();
Type jandexType = Type.create(dotName, Type.Kind.CLASS);
reflectiveHierarchyClass.produce(new ReflectiveHierarchyBuildItem(jandexType));
}
}
2.3.9. ビルドステップの依存関係の可視化
様々なビルドステップ間の相互作用を視覚的に表現することが役立つ場合があります。そのような場合、アプリケーションのビルド時に -Dquarkus.builder.graph-output=build.dot を追加すると、プロジェクトのルートディレクトリに build.dot ファイルが作成されます。ファイルを開いて実際の視覚的表現を表示できるソフトウェアの一覧については、 こちら を参照してください。
2.4. 設定
Quarkus の設定は SmallRye Config に基づいています。 SmallRye Config が提供するすべての機能は、 Quarkus でも利用可能です。
エクステンションは、エクステンションに必要な設定をマッピングするために SmallRye Config @ConfigMapping を使用する必要があります。これにより、 Quarkus は各設定フェーズにマッピングのインスタンスを自動的に公開し、設定ドキュメントを生成できるようになります。
2.4.1. 設定フェーズ
設定マッピングは設定フェーズによって厳密に拘束されており、対応するフェーズ以外から設定マッピングにアクセスしようとするとエラーになります。これらは、含まれるキーがいつ設定から読み取られ、いつアプリケーションで利用可能になるかを規定します。 io.quarkus.runtime.annotations.ConfigPhase によって定義されるフェーズは以下の通りです。
| フェーズ名 | ビルド時に読み取りおよび利用可能 | 実行時に利用可能 | 静的初期化中に読み取り | 起動時に再読み取り (ネイティブ実行可能ファイル) | 備考 |
|---|---|---|---|---|---|
|
✓ |
✗ |
✗ |
✗ |
ビルドに影響を与えるものに適しています。 |
|
✓ |
✓ |
✗ |
✗ |
ビルドに影響を与え、かつ実行時のコードからも参照可能である必要があるものに適しています。実行時に設定から読み取られることはありません。 |
|
✗ |
✓ |
✓ |
✓ |
ビルド時には利用できず、すべてのモードの起動時に読み取られます。 |
BUILD_TIME の場合を除き、すべてのケースにおいて、設定マッピングインターフェースおよびそこに含まれるすべての設定グループと型は、エクステンションのランタイムアーティファクト内にあるか、そこから到達可能である必要があります。フェーズ BUILD_TIME の設定マッピングは、エクステンションのランタイムまたはデプロイメントアーティファクトのいずれかにあるか、そこから到達可能であれば問題ありません。
2.4.2. 設定例
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import java.io.File;
import java.util.logging.Level;
/**
* Logging configuration.
*/
@ConfigMapping(prefix = "quarkus.log") (1)
@ConfigRoot(phase = ConfigPhase.RUN_TIME) (2)
public interface LogConfiguration {
// ...
/**
* Configuration properties for the logging file handler.
*/
FileConfig file();
interface FileConfig {
/**
* Enable logging to a file.
*/
@WithDefault("true")
boolean enabled();
/**
* The log format.
*/
@WithDefault("%d{yyyy-MM-dd HH:mm:ss,SSS} %h %N[%i] %-5p [%c{1.}] (%t) %s%e%n")
String format();
/**
* The level of logs to be written into the file.
*/
@WithDefault("ALL")
Level level();
/**
* The name of the file in which logs will be written.
*/
@WithDefault("application.log")
File path();
}
}
public class LoggingProcessor {
// ...
/*
* Logging configuration.
*/
LogConfiguration config; (3)
}
設定プロパティー名はセグメントに分割できます。例えば、 quarkus.log.file.enabled のようなプロパティー名は、以下のセグメントに分割できます。
-
quarkus- Quarkus によって予約された名前空間で、@ConfigMappingインターフェースのプレフィックスとなります。 -
log-@ConfigMappingでアノテーションが付けられたインターフェースで設定されたプレフィックスに対応する名前セグメント。 -
file- このクラスのfileフィールドに対応する名前セグメント。 -
enabled-FileConfigのenabledフィールドに対応する名前セグメント。
| 1 | @ConfigMapping アノテーションは、インターフェースが設定マッピングであることを示します。この例では quarkus.log セグメントに対応します。 |
| 2 | @ConfigRoot アノテーションは、その設定がどの設定フェーズに適用されるかを示します。 |
| 3 | ここでは、 LoggingProcessor が @ConfigRoot アノテーションを検出することで、 LogConfiguration インスタンスを自動的にインジェクトします。 |
上記の例に対応する application.properties は以下のようになります。
quarkus.log.file.enabled=true
quarkus.log.file.level=DEBUG
quarkus.log.file.path=/tmp/debug.log
format はこれらのプロパティーで定義されていないため、代わりに @WithDefault からのデフォルト値が使用されます。
複数の 設定フェーズ の設定マッピングがある場合に備えて、設定マッピング名には追加のサフィックスセグメントを含めることができます。 BUILD_TIME および BUILD_AND_RUN_TIME_FIXED に対応するクラスは BuildTimeConfig または BuildTimeConfiguration で終わり、 RUN_TIME フェーズに対応するクラスは RuntimeConfig 、 RunTimeConfig 、 RuntimeConfiguration 、または RunTimeConfiguration で終わるようにします。
2.4.3. 設定リファレンスドキュメント
設定は各エクステンションの重要な部分であるため、適切にドキュメント化する必要があります。各設定プロパティーには、適切な Javadoc コメントが必要です。
コーディング中にドキュメントを利用できるのは便利ですが、設定ドキュメントはエクステンションガイドでも利用可能である必要があります。 Quarkus のビルドは、 Javadoc コメントに基づいて設定ドキュメントを自動的に生成しますが、各ガイドに明示的に含める必要があります。
2.4.3.1. ドキュメントの記述
各設定プロパティーには、その目的を説明する Javadoc が必要です。
|
最初の文は、要約テーブルに含まれるため、意味があり自己完結している必要があります。 |
単純なドキュメントであれば標準的な Javadoc コメントで十分ですが (推奨もされます)、ヒント、ソースコードの抜粋、リストなどには AsciiDoc がより適しています。
/**
* Class name of the Hibernate ORM dialect. The complete list of bundled dialects is available in the
* https://docs.hibernate.org/stable/orm/javadocs/org/hibernate/dialect/package-summary.html[Hibernate ORM JavaDoc].
*
* [NOTE]
* ====
* Not all the dialects are supported in GraalVM native executables: we currently provide driver extensions for
* PostgreSQL, MariaDB, Microsoft SQL Server and H2.
* ====
*
* @asciidoclet
*/
Optional<String> dialect();
AsciiDoc を使用するには、 Javadoc コメントに @asciidoclet タグを付ける必要があります。このタグには 2 つの目的があります。 1 つは Quarkus 生成ツールのマーカーとして使用され、もう 1 つは Javadoc 生成のための javadoc プロセスによっても使用されます。
より詳細な例を以下に示します。
// @formatter:off
/**
* Name of the file containing the SQL statements to execute when Hibernate ORM starts.
* Its default value differs depending on the Quarkus launch mode:
*
* * In dev and test modes, it defaults to `import.sql`.
* Simply add an `import.sql` file in the root of your resources directory
* and it will be picked up without having to set this property.
* Pass `no-file` to force Hibernate ORM to ignore the SQL import file.
* * In production mode, it defaults to `no-file`.
* It means Hibernate ORM won't try to execute any SQL import file by default.
* Pass an explicit value to force Hibernate ORM to execute the SQL import file.
*
* If you need different SQL statements between dev mode, test (`@QuarkusTest`) and in production, use Quarkus
* https://quarkus.io/guides/config#configuration-profiles[configuration profiles facility].
*
* [source,property]
* .application.properties
* ----
* %dev.quarkus.hibernate-orm.sql-load-script = import-dev.sql
* %test.quarkus.hibernate-orm.sql-load-script = import-test.sql
* %prod.quarkus.hibernate-orm.sql-load-script = no-file
* ----
*
* [NOTE]
* ====
* Quarkus supports `.sql` file with SQL statements or comments spread over multiple lines.
* Each SQL statement must be terminated by a semicolon.
* ====
*
* @asciidoclet
*/
// @formatter:on
Optional<String> sqlLoadScript();
Javadoc コメント内でインデントを維持する場合 (複数行にわたるリスト項目やインデントされたソースコードなど)、マーカー // @formatter:off / // @formatter:on を使用して、自動 Eclipse フォーマッター (フォーマッターはビルドに自動的に含まれます) を無効にする必要があります。これらには個別のコメントが必要であり、 // マーカーの後にスペースが必須です。
|
AsciiDoc ドキュメントでは、オープンブロック ( |
|
デフォルトでは、ドキュメントジェネレーターはハイフンで繋がれたフィールド名を
ドキュメントのデフォルト値にテキストによる説明を書くことができます。これは値が生成される場合に便利です:
|
2.4.3.2. セクションドキュメントの記述
特定のグループの設定セクションを生成するには、 @ConfigDocSection アノテーションを使用します。
/**
* Config group related configuration.
* Amazing introduction here
*/
@ConfigDocSection (1)
ConfigGroupConfig configGroup();
| 1 | これにより、生成されたドキュメント内の configGroup 設定項目にセクションドキュメントが追加されます。セクションのタイトルと導入文は、設定項目の Javadoc から派生します。 Javadoc の最初の文がセクションタイトルと見なされ、残りの文がセクションの導入文として使用されます。 |
2.4.3.3. ドキュメントの生成
ドキュメントを生成するには、以下を実行します。
-
./mvnw -DquicklyDocsを実行する -
グローバルに、または特定のエクステンションディレクトリ (例:
extensions/mailer) で実行できます。
ドキュメントは、プロジェクトのルートにあるグローバルの target/asciidoc/generated/config/ に生成されます。
2.4.3.4. エクステンションガイドへのドキュメントの組み込み
生成された設定リファレンスドキュメントをガイドに含めるには、以下を使用します。
include::{generated-dir}/config/quarkus-your-extension.adoc[opts=optional, leveloffset=+1]
特定の設定グループのみを含めるには、以下を使用します。
include::{generated-dir}/hyphenated-config-group-class-name-with-runtime-or-deployment-namespace-replaced-by-config-group-namespace.adoc[opts=optional, leveloffset=+1]
例えば、 io.quarkus.vertx.http.runtime.FormAuthConfig 設定グループは、 quarkus-vertx-http-config-group-form-auth-config.adoc という名前のファイルに生成されます。
いくつかの推奨事項を以下に示します。
-
設定ドキュメントの一部しか生成されていない場合にビルドを失敗させないために、
opts=optionalが必須です。 -
ドキュメントはタイトルレベル 2 (すなわち
==) で生成されます。leveloffset=+Nで調整が必要になる場合があります。 -
設定ドキュメント全体をガイドの途中に含めるべきではありません。
ガイドに application.properties の例が含まれている場合は、コードスニペットのすぐ下にヒントを含める必要があります。
[TIP]
For more information about the extension configuration please refer to the <<configuration-reference,Configuration Reference>>.
そしてガイドの最後に、広範な設定ドキュメントを配置します。
[[configuration-reference]]
== Configuration Reference
include::{generated-dir}/config/quarkus-your-extension.adoc[opts=optional, leveloffset=+1]
|
すべてのドキュメントは、コミットする前に生成して検証する必要があります。 |
2.5. 条件付きステップの包含
特定の条件下でのみ、特定の @BuildStep を含めることが可能です。 @BuildStep アノテーションには、 onlyIf と onlyIfNot という 2 つのオプションパラメーターがあります。これらのパラメーターには、 BooleanSupplier を実装する 1 つ以上のクラスを設定できます。ビルドステップは、メソッドが true (onlyIf の場合) または false (onlyIfNot の場合) を返したときにのみ含まれます。
条件クラスは、ビルド時フェーズに属している限り、 設定マッピング をインジェクトできます。実行時設定は条件クラスでは利用できません。
条件クラスは io.quarkus.runtime.LaunchMode 型の値をインジェクトすることもできます。コンストラクターパラメーターおよびフィールドインジェクションがサポートされています。
@BuildStep(onlyIf = IsDevMode.class)
LogCategoryBuildItem enableDebugLogging() {
return new LogCategoryBuildItem("org.your.quarkus.extension", Level.DEBUG);
}
static class IsDevMode implements BooleanSupplier {
LaunchMode launchMode;
public boolean getAsBoolean() {
return launchMode == LaunchMode.DEVELOPMENT;
}
}
| 別のエクステンションの有無を条件にビルドステップを実行する必要がある場合は、そのために Capability を使用できます。 |
@BuildSteps を使用して、特定のクラス内のすべてのビルドステップに一連の条件を適用することもできます。
@BuildSteps(onlyIf = MyDevModeProcessor.IsDevMode.class) (1)
class MyDevModeProcessor {
@BuildStep
SomeOutputBuildItem mainBuildStep(SomeOtherBuildItem input) { (2)
return new SomeOutputBuildItem(input.getValue());
}
@BuildStep
SomeOtherOutputBuildItem otherBuildStep(SomeOtherInputBuildItem input) { (3)
return new SomeOtherOutputBuildItem(input.getValue());
}
static class IsDevMode implements BooleanSupplier {
LaunchMode launchMode;
public boolean getAsBoolean() {
return launchMode == LaunchMode.DEVELOPMENT;
}
}
}
| 1 | この条件は MyDevModeProcessor で定義されているすべてのメソッドに適用されます。 |
| 2 | メインのビルドステップは開発モードでのみ実行されます。 |
| 3 | もう一方のビルドステップも開発モードでのみ実行されます。 |
2.6. バイトコードの生成
2.6.1. バイトコードレコーディング
ビルドプロセスの主な出力の 1 つは、記録されたバイトコードです。このバイトコードが実際にランタイム環境をセットアップします。例えば、 Undertow を起動するために、結果として得られるアプリケーションには、すべての Servlet インスタンスを直接登録し、その後に Undertow を起動するバイトコードが含まれます。
バイトコードを直接記述するのは複雑なため、代わりにバイトコードレコーダーを介して行われます。デプロイ時に、実際のランタイムロジックを含むレコーダーオブジェクトに対して呼び出しが行われますが、これらの呼び出しは通常通り進行するのではなく、インターセプトされて記録されます (これが名前の由来です)。この記録は、実行時に同じ一連の呼び出しを実行するバイトコードを生成するために使用されます。これは本質的に、デプロイ時に行われた呼び出しを実行時まで遅延させる、遅延実行の一種です。
古典的な「Hello World」タイプの例を見てみましょう。これを Quarkus 方式で行うには、以下のようにレコーダーを作成します。
@Recorder
class HelloRecorder {
public void sayHello(String name) {
System.out.println("Hello" + name);
}
}
次に、このレコーダーを使用するビルドステップを作成します。
@Record(RUNTIME_INIT)
@BuildStep
public void helloBuildStep(HelloRecorder recorder) {
recorder.sayHello("World");
}
このビルドステップを実行しても、コンソールには何も表示されません。これは、インジェクトされた HelloRecorder が、実際にはすべての呼び出しを記録するプロキシであるためです。代わりに、生成された Quarkus プログラムを実行すると、コンソールに「Hello World」と表示されます。
レコーダーのメソッドは値を返すことができますが、それはプロキシ可能である必要があります (プロキシ不可能なアイテムを返したい場合は、 io.quarkus.runtime.RuntimeValue でラップしてください)。これらのプロキシは直接呼び出すことはできませんが、他のレコーダーメソッドに渡すことができます。これは他の @BuildStep メソッドからのものを含む、あらゆるレコーダーメソッドに渡すことができるため、これらのレコーダー呼び出しの結果をラップする BuildItem インスタンスを生成するのが一般的なパターンです。
例えば、 Servlet デプロイメントに任意の変更を加えるために、 Undertow には ServletExtensionBuildItem があります。これは ServletExtension インスタンスをラップする MultiBuildItem です。別のモジュールのレコーダーから ServletExtension を返すことができ、 Undertow はそれを消費して、 Undertow を起動するレコーダーメソッドに渡します。
実行時に、バイトコードは生成された順序で呼び出されます。これは、ビルドステップの依存関係が、生成されたバイトコードが実行される順序を暗黙的に制御することを意味します。上記の例では、 ServletExtensionBuildItem を生成するバイトコードが、それを消費するバイトコードよりも前に実行されることがわかります。
以下のオブジェクトをレコーダーに渡すことができます。
-
プリミティブ型
-
String
-
Class<?> オブジェクト
-
以前のレコーダー呼び出しから返されたオブジェクト
-
引数なしのコンストラクターと、すべてのプロパティーのゲッター/セッター (またはパブリックフィールド) を持つオブジェクト
-
フィールド名と一致するパラメーター名を持つ、
@RecordableConstructorアノテーションが付与されたコンストラクターを持つオブジェクト -
io.quarkus.deployment.recording.RecorderContext#registerSubstitution(Class, Class, Class)メカニズムを介した任意のオブジェクト -
上記を要素とする配列、 List、および Map
|
記録対象のオブジェクトのいくつかのフィールドを無視する必要がある場合 (すなわち、ビルド時の値を実行時に反映させるべきではない場合)、フィールドに クラスが Quarkus に依存できない場合、エクステンションが この同じ SPI を使用して、 |
2.6.2. レコーダーへの設定のインジェクト
フェーズが RUNTIME または BUILD_AND_RUNTIME_FIXED の設定オブジェクトは、コンストラクターインジェクションを介してレコーダーにインジェクトできます。コンストラクターには、各設定オブジェクト型に対応するパラメーターが必要です。設定オブジェクト型が RUNTIME フェーズであると宣言されている場合は、 RuntimeValue<> 型でラップする必要があります。
@ConfigMapping(prefix = "quarkus.btrt")
@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUNTIME_FIXED)
public interface BuildAndRuntimeFixedConfig {
}
@ConfigMapping(prefix = "quarkus.rt")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public interface RuntimeConfig {
}
@Recorder
class ExtensionRecorder {
private final BuildAndRuntimeFixedConfig buildAndRuntimeFixedConfig;
private final RuntimeValue<RuntimeConfig> runtimeConfig;
public ExtensionRecorder(
BuildAndRuntimeFixedConfig buildAndRuntimeFixedConfig,
RuntimeValue<RuntimeConfig> runtimeConfig) {
this.buildAndRuntimeFixedConfig = buildAndRuntimeFixedConfig;
this.runtimeConfig = runtimeConfig;
}
}
レコーダーに複数のコンストラクターがある場合は、 Quarkus に使用させたいものに @Inject を付与できます。
|
2.6.3. RecorderContext
io.quarkus.deployment.recording.RecorderContext は、バイトコードレコーディングを強化するための便利なメソッドを提供します。これには、引数なしのコンストラクターを持たないクラスの作成関数を登録する機能、オブジェクト置換 (基本的にはシリアライズ不可能なオブジェクトからシリアライズ可能なオブジェクトへの、あるいはその逆のトランスフォーマー) を登録する機能、およびクラスプロキシを作成する機能が含まれます。このインターフェースは、任意の @Record メソッドのメソッドパラメーターとして直接インジェクトできます。
指定された完全修飾クラス名で classProxy を呼び出すと、レコーダーメソッドに渡すことができる Class インスタンスが作成され、実行時には classProxy() に渡された名前のクラスに置き換えられます。ただし、ビルドステップの処理時にデプロイメント/アプリケーションクラスを直接ロードすることは安全であるため、ほとんどのユースケースでこのメソッドは必要ありません。そのため、このメソッドは非推奨になっています。それにもかかわらず、 GeneratedClassBuildItem を使用して以前のビルドステップで生成されたクラスを参照する場合など、このメソッドが役立つユースケースもいくつか存在します。
2.6.3.1. ステップの実行時間の表示
アプリケーションの実行時に、各スタートアップタスク (各バイトコードレコーディングの結果) にかかる正確な時間を知ることが役立つ場合があります。この情報を確認する最も簡単な方法は、 -Dquarkus.debug.print-startup-times=true システムプロパティーを指定して Quarkus アプリケーションを起動することです。出力は以下のようになります。
Build step LoggingResourceProcessor.setupLoggingRuntimeInit completed in: 42ms
Build step ConfigGenerationBuildStep.checkForBuildTimeConfigChange completed in: 4ms
Build step SyntheticBeansProcessor.initRuntime completed in: 0ms
Build step ConfigBuildStep.validateConfigProperties completed in: 1ms
Build step ResteasyStandaloneBuildStep.boot completed in: 95ms
Build step VertxHttpProcessor.initializeRouter completed in: 1ms
Build step VertxHttpProcessor.finalizeRouter completed in: 4ms
Build step LifecycleEventsBuildStep.startupEvent completed in: 1ms
Build step VertxHttpProcessor.openSocket completed in: 93ms
Build step ShutdownListenerBuildStep.setupShutdown completed in: 1ms
2.6.4. Gizmo の使用
シナリオによっては、より大幅なバイトコードの操作が必要になる場合があります。バイトコードレコーディングだけでは不十分な場合、 Gizmo は ASM に代わる便利な選択肢であり、よりハイレベルな API を備えています。
2.7. リソースの生成
エクステンションを使用してリソースを生成することが可能です。シナリオによっては、 META-INF ディレクトリにリソースを生成する必要があり、そのリソースは SPI 用のサービスであったり、単純な HTML 、 CSS 、 JavaScript ファイルであったりします。
/**
* This build step aggregates all the produced service providers
* and outputs them as resources.
*/
@BuildStep
public void produceServiceFiles(
BuildProducer<GeneratedStaticResourceBuildItem> generatedStaticResourceProducer,
BuildProducer<GeneratedResourceBuildItem> generatedResourceProducer
) throws IOException {
generatedResourceProducer.produce( (1)
new GeneratedResourceBuildItem(
"META-INF/services/io.quarkus.services.GreetingService",
"""
public class HelloService implements GreetingService {
@Override
public void do() {
System.out.println("Hello!");
}
}
""".getBytes(StandardCharsets.UTF_8)));
generatedStaticResourceProducer.produce( (2)
new GeneratedStaticResourceBuildItem(
"/index.js",
"console.log('Hello World!')".getBytes(StandardCharsets.UTF_8))
);
}
-
META-INF/services のリソースとして SPI サービス実装を生成する
-
Vert.x によって提供される静的リソース (例: JavaScript ファイル) を生成する
2.7.1. 主要なポイント
-
GeneratedResourceBuildItem-
本番モードで永続化されるリソースを生成します。
-
開発モードやその他の非本番モードでは、リソースはメモリー内に保持され、
QuarkusClassLoaderを使用してロードされます。
-
-
GeneratedStaticResourceBuildItem-
Vert.x によって提供される静的リソース (JavaScript 、 HTML 、 CSS などのファイル) を生成します。
-
開発モードでは、 Quarkus はクラスローダーベースのファイルシステムに裏打ちされた Vert.x ハンドラーを使用して、これらのリソースを提供します。
-
2.7.2. GeneratedResourceBuildItem と GeneratedStaticResourceBuildItem の違い
どちらもリソースの生成に使用されますが、その目的と動作は異なります。
GeneratedResourceBuildItem:
-
実行時に必要なリソース (例: SPI サービス定義) に使用されます。
-
本番モードでのみ永続化され、それ以外の場合はメモリーに保存されます。
GeneratedStaticResourceBuildItem:
-
HTTP 経由で静的リソース (例: JavaScript や CSS ファイル) を提供するために設計されています。
-
開発モードでは、これらのリソースは Vert.x を使用して動的に提供されます。
-
GeneratedResourceBuildItemを生成します。 -
通常モードでのみ
AdditionalStaticResourceBuildItemを生成します。
これらのビルドアイテムを適切に使用することで、 Quarkus エクステンション内でリソースを効果的に生成および管理できます。
2.8. Contexts and Dependency Injection (CDI)
CDI 統合ガイド には、一般的な CDI 関連のユースケースの詳細と、解決策のサンプルコードが記載されています。
2.8.1. 拡張ポイント
CDI ベースのランタイムとして、 Quarkus エクステンションはしばしばエクステンションの動作の一部として CDI Bean を利用可能にします。しかし、 Quarkus の DI ソリューションは CDI Portable Extensions をサポートしていません。代わりに、 Quarkus エクステンションは様々な ビルド時拡張ポイント を利用できます。
2.9. Quarkus Dev UI
より優れた開発体験のために、エクステンションで Quarkus Dev UI をサポートさせることができます。
2.10. エクステンション定義のエンドポイント
エクステンションは、 Health 、 Metrics 、 OpenAPI 、 Swagger UI などのエンドポイントと並行して提供される、追加の非アプリケーションエンドポイントを追加できます。
NonApplicationRootPathBuildItem を使用してエンドポイントを定義します。
@BuildStep
RouteBuildItem myExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
return nonApplicationRootPathBuildItem.routeBuilder()
.route("custom-endpoint")
.handler(new MyCustomHandler())
.displayOnNotFoundPage()
.build();
}
パスの先頭に '/' が付いていないことは、それが相対パスであることを示していることに注意してください。上記のエンドポイントは、設定された非アプリケーションエンドポイントのルートからの相対パスで提供されます。非アプリケーションエンドポイントのルートはデフォルトで /q であるため、結果としてエンドポイントは /q/custom-endpoint に配置されます。
絶対パスは異なった扱いになります。上記で route("/custom-endpoint") が呼び出された場合、結果としてエンドポイントは /custom-endpoint に配置されます。
エクステンションでネストされた非アプリケーションエンドポイントが必要な場合。
@BuildStep
RouteBuildItem myNestedExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
return nonApplicationRootPathBuildItem.routeBuilder()
.nestedRoute("custom-endpoint", "deep")
.handler(new MyCustomHandler())
.displayOnNotFoundPage()
.build();
}
非アプリケーションエンドポイントのルートがデフォルトの /q の場合、これにより /q/custom-endpoint/deep にエンドポイントが作成されます。
絶対パスは、ネストされたエンドポイントにも影響を与えます。上記で nestedRoute("custom-endpoint", "/deep") が呼び出された場合、結果としてエンドポイントは /deep に配置されます。
非アプリケーションのルートパスがどのように設定されるかの詳細については、 Quarkus Vertx HTTP 設定リファレンス を参照してください。
2.11. エクステンションのヘルスチェック
ヘルスチェックは quarkus-smallrye-health エクステンションを介して提供されます。これは liveness probe と readiness probe の両方の機能を提供します。
エクステンションを作成する際、エクステンション独自のヘルスチェックを提供すると、開発者が自分で記述しなくても自動的に組み込まれるようになるため有益です。
ヘルスチェックを提供するには、以下を行う必要があります。
-
ヘルスチェックが含まれていない場合にアプリケーションのサイズに影響を与えないよう、
quarkus-smallrye-healthエクステンションをランタイムモジュールの オプション の依存関係としてインポートします。 -
SmallRye Health ガイドに従ってヘルスチェックを作成します。エクステンションには readiness probe のみを提供することをお勧めします (liveness probe はアプリケーションが起動しているという事実を表現するためのものであり、軽量である必要があります)。
-
デプロイメントモジュールに
quarkus-smallrye-health-spiライブラリーをインポートします。 -
デプロイメントモジュールに
HealthBuildItemを生成するビルドステップを追加します。 -
設定項目
quarkus.<extension>.health.enabledを介してエクステンションのヘルスチェックを無効にする方法を追加します。これはデフォルトで有効にする必要があります。
以下は、データソースの readiness を検証するための DataSourceHealthCheck を提供する Agroal エクステンションの例です。
@BuildStep
HealthBuildItem addHealthCheck(AgroalBuildTimeConfig agroalBuildTimeConfig) {
return new HealthBuildItem("io.quarkus.agroal.runtime.health.DataSourceHealthCheck",
agroalBuildTimeConfig.healthEnabled);
}
2.12. エクステンションのメトリクス
quarkus-micrometer エクステンションはメトリクスの収集をサポートします。互換性に関する注意として、 quarkus-micrometer エクステンションは MP Metrics API を Micrometer ライブラリーのプリミティブに適合させるため、 MP Metrics API に依存するコードを壊すことなく quarkus-micrometer エクステンションを有効にできます。 Micrometer によって出力されるメトリクスは異なることに注意してください。詳細については quarkus-micrometer エクステンションのドキュメントを参照してください。
| MP Metrics API の互換レイヤーは、将来的に別のエクステンションに移動する予定です。 |
エクステンションがオプションのメトリクスエクステンションとやり取りして独自のメトリクスを追加するには、大きく分けて 2 つのパターンがあります。
-
コンシューマーパターン: エクステンションは
MetricsFactoryConsumerBuildItemを宣言し、それを使用してバイトコードレコーダーをメトリクスエクステンションに提供します。メトリクスエクステンションが初期化されると、登録されたコンシューマーを反復処理して、それらをMetricsFactoryで初期化します。このファクトリーは API に依存しないメトリクスを宣言するために使用でき、統計収集のためのインストゥルメント可能なオブジェクトを提供するエクステンション (例: Hibernate のStatisticsクラス) に適しています。 -
バインダーパターン: エクステンションは、メトリクスシステムに応じて完全に異なる収集実装を使用することを選択できます。
Optional<MetricsCapabilityBuildItem> metricsCapabilityビルドステップパラメーターを使用して、アクティブなメトリクスエクステンション (例: "micrometer") に基づいて API 固有のメトリクスを宣言または初期化できます。このパターンは、レコーダー内でMetricsFactory::metricsSystemSupported()を使用してアクティブなメトリクスエクステンションをテストすることにより、コンシューマーパターンと組み合わせることができます。
メトリクスのサポートはオプションであることを忘れないでください。エクステンションは、ビルドステップで Optional<MetricsCapabilityBuildItem> metricsCapability パラメーターを使用して、有効なメトリクスエクステンションの有無をテストできます。メトリクスの動作を制御するために追加の設定を使用することを検討してください。例えば、データソースのメトリクスは負荷が高くなる可能性があるため、個別のデータソースでメトリクス収集を有効にするために追加の設定フラグが使用されます。
エクステンションにメトリクスを追加する際、以下のいずれかの状況に当てはまる場合があります。
-
エクステンションで使用されている基盤となるライブラリーが、特定のメトリクス API (MP Metrics 、 Micrometer 、またはその他) を直接使用している。
-
基盤となるライブラリーがメトリクス収集のために独自のメカニズムを使用しており、独自の API (例: Hibernate の
Statisticsクラス、または Vert.x のMetricsOptions) を介して実行時にそれらを利用できるようにしている。 -
基盤となるライブラリーがメトリクスを提供していない (またはライブラリー自体が存在しない) ため、インストゥルメンテーションを追加したい。
2.12.1. ケース 1: ライブラリーがメトリクスライブラリーを直接使用する場合
ライブラリーがメトリクス API を直接使用する場合、2 つのオプションがあります。
-
Optional<MetricsCapabilityBuildItem> metricsCapabilityパラメーターを使用して、ビルドステップでどのメトリクス API (例: "micrometer") がサポートされているかをテストし、それを使用して API 固有の Bean またはビルドアイテムを選択的に宣言または初期化します。 -
MetricsFactoryを使用する個別のビルドステップを作成し、バイトコードレコーダー内でMetricsFactory::metricsSystemSupported()メソッドを使用して、目的のメトリクス API (例: "micrometer") がサポートされている場合に、必要なリソースを初期化します。
アクティブなメトリクスエクステンションがない場合、またはエクステンションがライブラリーに必要な API をサポートしていない場合に備えて、エクステンションはフォールバックを提供する必要があるかもしれません。
2.12.2. ケース 2: ライブラリーが独自のメトリクス API を提供する場合
ライブラリーが独自のメトリクス API を提供する例は 2 つあります。
-
Agroal が
io.agroal.api.AgroalDataSourceMetricsで行っているように、エクステンションが計測可能なオブジェクトを定義する。 -
Jaeger が
io.jaegertracing.spi.MetricsFactoryで行っているように、エクステンションがメトリクスの独自の抽象化を提供する。
2.12.2.1. 計測可能なオブジェクトの監視
まず、計測可能なオブジェクト (io.agroal.api.AgroalDataSourceMetrics) のケースを見てみましょう。この場合、以下を行うことができます。
-
RUNTIME_INITまたはSTATIC_INITレコーダーを使用してMetricsFactoryコンシューマーを定義する、MetricsFactoryConsumerBuildItemを生成するBuildStepを定義します。たとえば、以下は Agroal 全体と特定のデータソースの両方でメトリクスが有効である場合にのみ、MetricsFactoryConsumerBuildItemを作成します。@BuildStep @Record(ExecutionTime.RUNTIME_INIT) void registerMetrics(AgroalMetricsRecorder recorder, DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, BuildProducer<MetricsFactoryConsumerBuildItem> datasourceMetrics, List<AggregatedDataSourceBuildTimeConfigBuildItem> aggregatedDataSourceBuildTimeConfigs) { for (AggregatedDataSourceBuildTimeConfigBuildItem aggregatedDataSourceBuildTimeConfig : aggregatedDataSourceBuildTimeConfigs) { // Create a MetricsFactory consumer to register metrics for a data source // IFF metrics are enabled globally and for the data source // (they are enabled for each data source by default if they are also enabled globally) if (dataSourcesBuildTimeConfig.metricsEnabled && aggregatedDataSourceBuildTimeConfig.getJdbcConfig().enableMetrics.orElse(true)) { datasourceMetrics.produce(new MetricsFactoryConsumerBuildItem( recorder.registerDataSourceMetrics(aggregatedDataSourceBuildTimeConfig.getName()))); } } } -
関連するレコーダーは、提供された
MetricsFactoryを使用してメトリクスを登録する必要があります。Agroal の場合、これはMetricFactoryAPI を使用してio.agroal.api.AgroalDataSourceMetricsメソッドを監視することを意味します。例えば、以下のようになります。/* RUNTIME_INIT */ public Consumer<MetricsFactory> registerDataSourceMetrics(String dataSourceName) { return new Consumer<MetricsFactory>() { @Override public void accept(MetricsFactory metricsFactory) { String tagValue = DataSourceUtil.isDefault(dataSourceName) ? "default" : dataSourceName; AgroalDataSourceMetrics metrics = getDataSource(dataSourceName).getMetrics(); // When using MP Metrics, the builder uses the VENDOR registry by default. metricsFactory.builder("agroal.active.count") .description( "Number of active connections. These connections are in use and not available to be acquired.") .tag("datasource", tagValue) .buildGauge(metrics::activeCount); ....
MetricsFactory はメトリクス登録のための流れるようなビルダーを提供し、最終ステップで Supplier または ToDoubleFunction に基づいてゲージまたはカウンターを構築します。タイマーは、Callable、Runnable、または Supplier の実装をラップすることも、TimeRecorder を使用して時間の断片を蓄積することもできます。基盤となるメトリクスエクステンションは、定義された関数を監視または測定するための適切なアーティファクトを作成します。
2.12.2.2. メトリクス API 固有の実装の使用
場合によっては、メトリクス API 固有の実装を使用することが好ましいことがあります。たとえば Jaeger は、カウンターとゲージを定義するために使用する独自のメトリクスインターフェイス io.jaegertracing.spi.MetricsFactory を定義しています。そのインターフェイスからメトリクスシステムへの直接のマッピングが最も効率的です。この場合、これらの特殊な実装を分離し、早期のクラスロードを避けて、メトリクス API がオプションのコンパイル時依存関係として維持されるようにすることが重要です。
Optional<MetricsCapabilityBuildItem> metricsCapability をビルドステップで使用して、Bean の初期化や他のビルドアイテムの生成を選択的に制御できます。たとえば Jaeger エクステンションは、以下を使用して特殊なメトリクス API アダプターの初期化を制御できます。
+
/* RUNTIME_INIT */
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void setupTracer(JaegerDeploymentRecorder jdr, JaegerBuildTimeConfig buildTimeConfig, JaegerConfig jaeger,
ApplicationConfig appConfig, Optional<MetricsCapabilityBuildItem> metricsCapability) {
// Indicates that this extension would like the SSL support to be enabled
extensionSslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.JAEGER.getName()));
if (buildTimeConfig.enabled) {
// To avoid dependency creep, use two separate recorder methods for the two metrics systems
if (buildTimeConfig.metricsEnabled && metricsCapability.isPresent()) {
if (metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)) {
jdr.registerTracerWithMicrometerMetrics(jaeger, appConfig);
} else {
jdr.registerTracerWithMpMetrics(jaeger, appConfig);
}
} else {
jdr.registerTracerWithoutMetrics(jaeger, appConfig);
}
}
}
同様の方法で、MetricsFactory を使用するレコーダーは MetricsFactory::metricsSystemSupported() を使用して、バイトコードの記録中にメトリクスオブジェクトの初期化を制御できます。
2.12.3. ケース 3: エクステンションコード内でメトリクスを収集する必要がある場合
独自のメトリクスを一から定義するには、基本的に 2 つのオプションがあります。汎用的な MetricFactory ビルダーを使用するか、バインダーパターンに従って、有効なメトリクスエクステンションに固有のインストルメンテーションを作成します。
エクステンションに依存しない MetricFactory API を使用するには、プロセッサーで RUNTIME_INIT または STATIC_INIT レコーダーを使用して MetricsFactory コンシューマーを定義する、MetricsFactoryConsumerBuildItem を生成する BuildStep を定義できます。
+
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
MetricsFactoryConsumerBuildItem registerMetrics(MyExtensionRecorder recorder) {
return new MetricsFactoryConsumerBuildItem(recorder.registerMetrics());
}
+
- 関連するレコーダーは、提供された MetricsFactory を使用してメトリクスを登録する必要があります。例:
+
final LongAdder extensionCounter = new LongAdder();
/* RUNTIME_INIT */
public Consumer<MetricsFactory> registerMetrics() {
return new Consumer<MetricsFactory>() {
@Override
public void accept(MetricsFactory metricsFactory) {
metricsFactory.builder("my.extension.counter")
.buildGauge(extensionCounter::longValue);
....
メトリクスエクステンションはオプションであることを忘れないでください。メトリクス関連の初期化は、エクステンションの他のセットアップから分離し、メトリクス API の早期インポートを避けるようにコードを構成してください。メトリクスの収集はコストがかかる場合もあります。メトリクスサポートの有無だけでは不十分な場合は、メトリクスの動作を制御するためにエクステンション固有の追加設定を使用することを検討してください。
2.13. エクステンションからの JSON 処理のカスタマイズ
エクステンションは、エクステンションが提供する型に対してシリアライザーやデシリアライザーを登録する必要があることがよくあります。
このために、Jackson と JSON-B の両方のエクステンションは、エクステンションのデプロイメントモジュール内からシリアライザー/デシリアライザーを登録する方法を提供しています。
すべての人が JSON を必要とするわけではないため、オプションにする必要があることに注意してください。
エクステンションが JSON 関連のカスタマイズを提供する予定がある場合は、Jackson と JSON-B の両方のカスタマイズを提供することを強く推奨します。
2.13.1. Jackson のカスタマイズ
まず、エクステンションのランタイムモジュールに quarkus-jackson への オプション の依存関係を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
<optional>true</optional>
</dependency>
次に、Jackson 用のシリアライザーまたはデシリアライザー (あるいはその両方) を作成します。その例は mongodb-panache エクステンションで見ることができます。
public class ObjectIdSerializer extends StdSerializer<ObjectId> {
public ObjectIdSerializer() {
super(ObjectId.class);
}
@Override
public void serialize(ObjectId objectId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (objectId != null) {
jsonGenerator.writeString(objectId.toString());
}
}
}
エクステンションのデプロイメントモジュールに quarkus-jackson-spi への依存関係を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson-spi</artifactId>
</dependency>
JacksonModuleBuildItem を介して Jackson モジュールを登録するためのビルドステップをプロセッサーに追加します。モジュール名は、すべての Jackson モジュール間で一意である必要があります。
@BuildStep
JacksonModuleBuildItem registerJacksonSerDeser() {
return new JacksonModuleBuildItem.Builder("ObjectIdModule")
.add(io.quarkus.mongodb.panache.jackson.ObjectIdSerializer.class.getName(),
io.quarkus.mongodb.panache.jackson.ObjectIdDeserializer.class.getName(),
ObjectId.class.getName())
.build();
}
Jackson エクステンションは、生成されたビルドアイテムを使用して、Jackson 内にモジュールを自動的に登録します。
モジュールの登録よりも詳細なカスタマイズが必要な場合は、AdditionalBeanBuildItem を介して io.quarkus.jackson.ObjectMapperCustomizer を実装する CDI Bean を生成できます。Jackson のカスタマイズに関する詳細は、JSON ガイドの JSON サポートの設定 を参照してください。
2.13.2. JSON-B のカスタマイズ
まず、エクステンションのランタイムモジュールに quarkus-jsonb への オプション の依存関係を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb</artifactId>
<optional>true</optional>
</dependency>
次に、JSON-B 用のシリアライザーやデシリアライザーを作成します。その例は mongodb-panache エクステンションで見ることができます。
public class ObjectIdSerializer implements JsonbSerializer<ObjectId> {
@Override
public void serialize(ObjectId obj, JsonGenerator generator, SerializationContext ctx) {
if (obj != null) {
generator.write(obj.toString());
}
}
}
エクステンションのデプロイメントモジュールに quarkus-jsonb-spi への依存関係を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb-spi</artifactId>
</dependency>
JsonbSerializerBuildItem を介してシリアライザーを登録するためのビルドステップをプロセッサーに追加します。
@BuildStep
JsonbSerializerBuildItem registerJsonbSerializer() {
return new JsonbSerializerBuildItem(io.quarkus.mongodb.panache.jsonb.ObjectIdSerializer.class.getName()));
}
JSON-B エクステンションは、生成されたビルドアイテムを使用して、シリアライザー/デシリアライザーを自動的に登録します。
シリアライザーやデシリアライザーの登録よりも詳細なカスタマイズが必要な場合は、AdditionalBeanBuildItem を介して io.quarkus.jsonb.JsonbConfigCustomizer を実装する CDI Bean を生成できます。JSON-B のカスタマイズに関する詳細は、JSON ガイドの JSON サポートの設定 を参照してください。
2.14. 開発モードとの統合
開発モードと統合し、現在の状態に関する情報を取得するために使用できる様々な API があります。
2.14.1. 再起動の処理
Quarkus の起動時には、io.quarkus.deployment.builditem.LiveReloadBuildItem が存在することが保証されており、この起動に関する情報、特に以下の情報を取得できます。
-
これがクリーンな起動かライブリロードか
-
ライブリロードの場合、どのファイルの変更/クラスがリロードをトリガーしたか
また、静的フィールドを使用することなく、再起動間で情報を保存するために使用できるグローバルなコンテキストマップも提供されます。
以下は、ライブリロードをまたいでコンテキストを維持するビルドステップの例です。
@BuildStep(onlyIf = {IsDevelopment.class})
public void keyPairDevService(LiveReloadBuildItem liveReloadBuildItem, BuildProducer<KeyPairBuildItem> keyPairs) {
KeyPairContext ctx = liveReloadBuildItem.getContextObject(KeyPairContext.class); (1)
if (ctx == null && !liveReloadBuildItem.isLiveReload()) { (2)
KeyPair keyPair = generateKeyPair(2048);
Map<String, String> properties = generateDevServiceProperties(keyPair);
liveReloadBuildItem.setContextObject( (3)
KeyPairContext.class, new KeyPairContext(properties));
keyPairs.produce(new KeyPairBuildItem(properties));
}
if (ctx != null) {
Map<String, String> properties = ctx.getProperties();
keyPairs.produce(new KeyPairBuildItem(properties));
}
}
static record KeyPairContext(Map<String, String> properties) {}
| 1 | コンテキストは LiveReloadBuildItem から取得できます。指定された型のコンテキストがない場合、この呼び出しは null を返します。それ以外の場合は、前回のライブリロード実行時に保存されたインスタンスを返します。 |
| 2 | これが最初の実行 (ライブリロードではない) かどうかを確認できます。 |
| 3 | LiveReloadBuildItem#setContextObject メソッドを使用すると、ライブリロードをまたいでコンテキストを設定できます。 |
2.14.2. ライブリロードのトリガー
ライブリロードは通常 HTTP リクエストによってトリガーされますが、すべてのアプリケーションが HTTP アプリケーションであるとは限らず、一部のエクステンションは他のイベントに基づいてライブリロードをトリガーしたい場合があります。これを行うには、ランタイムモジュールで io.quarkus.dev.spi.HotReplacementSetup を実装し、その実装をリストした META-INF/services/io.quarkus.dev.spi.HotReplacementSetup を追加する必要があります。
起動時に setupHotDeployment メソッドが呼び出され、提供された io.quarkus.dev.spi.HotReplacementContext を使用して変更されたファイルのスキャンを開始できます。
2.14.3. Dev Services
エクステンションが外部サービスを使用する場合、Dev Service を追加すると、開発およびテストモードでのユーザーエクスペリエンスが向上します。詳細は、Dev Service の書き方 を参照してください。
2.15. エクステンションのテスト
Testing of Quarkus extensions should be done with the io.quarkus.test.QuarkusExtensionTest JUnit extension.
This extension allows for Arquillian-style tests that test specific functionalities.
It is not intended for testing user applications, as this should be done via io.quarkus.test.junit.QuarkusTest.
The main difference is that QuarkusTest simply boots the application once at the start of the run, while QuarkusExtensionTest deploys a custom
Quarkus application for each test class.
これらのテストはデプロイメントモジュールに配置する必要があります。テストに追加の Quarkus モジュールが必要な場合は、それらのデプロイメントモジュールもテストスコープの依存関係として追加する必要があります。
Note that QuarkusExtensionTest is in the quarkus-junit-internal module.
テストクラスの例は以下のようになります。
package io.quarkus.health.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import io.quarkus.test.QuarkusExtensionTest;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import io.restassured.RestAssured;
public class FailingUnitTest {
@RegisterExtension (1)
static final QuarkusExtensionTest config = new QuarkusExtensionTest()
.setArchiveProducer(() ->
ShrinkWrap.create(JavaArchive.class) (2)
.addClasses(FailingHealthCheck.class)
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")
);
@Inject (3)
@Liveness
Instance<HealthCheck> checks;
@Test
public void testHealthServlet() {
RestAssured.when().get("/q/health").then().statusCode(503); (4)
}
@Test
public void testHealthBeans() {
List<HealthCheck> check = new ArrayList<>(); (5)
for (HealthCheck i : checks) {
check.add(i);
}
assertEquals(1, check.size());
assertEquals(HealthCheckResponse.State.DOWN, check.get(0).call().getState());
}
}
| 1 | The QuarkusExtensionTest extension must be used with a static field. If used with a non-static field, the test application is not started. |
| 2 | このプロデューサーは、テスト対象のアプリケーションをビルドするために使用されます。Shrinkwrap を使用して、テスト用の JavaArchive を作成します。 |
| 3 | テストデプロイメントから Bean をテストケースに直接インジェクションすることが可能です。 |
| 4 | このメソッドはヘルスチェックサーブレットを直接呼び出し、レスポンスを検証します。 |
| 5 | このメソッドは、インジェクションされたヘルスチェック Bean を使用して、期待通りの結果を返していることを検証します。 |
エクステンションがビルド時に適切に失敗することをテストしたい場合は、setExpectedException メソッドを使用します。
package io.quarkus.hibernate.orm;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusExtensionTest;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
public class PersistenceAndQuarkusConfigTest {
@RegisterExtension
static QuarkusExtensionTest runner = new QuarkusExtensionTest()
.setExpectedException(ConfigurationException.class) (1)
.withApplicationRoot((jar) -> jar
.addAsManifestResource("META-INF/some-persistence.xml", "persistence.xml")
.addAsResource("application.properties"));
@Test
public void testPersistenceAndConfigTest() {
// should not be called, deployment exception should happen first:
// it's illegal to have Hibernate configuration properties in both the
// application.properties and in the persistence.xml
Assertions.fail();
}
}
| 1 | これにより、Quarkus のデプロイが特定の例外で失敗する必要があることを JUnit に伝えます。 |
|
Test coverage and
QuarkusExtensionTestランタイムサブモジュールのテストカバレッジを測定したい場合は、デプロイメントモジュールで特定の JaCoCo 設定が必要です。
デフォルトでは、JaCoCo は収集したデータを |
2.16. ホットリロードのテスト
開発モードでエクステンションが正しく動作し、更新を正しく処理できることを検証するテストを書くことも可能です。
ほとんどのエクステンションでは、これは「そのまま」動作しますが、この機能が期待通りに動作していることを検証するためのスモークテストを行うことは依然として良い考えです。これをテストするには QuarkusDevModeTest を使用します。
public class ServletChangeTestCase {
@RegisterExtension
final static QuarkusDevModeTest test = new QuarkusDevModeTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class) (1)
.addClass(DevServlet.class)
.addAsManifestResource(new StringAsset("Hello Resource"), "resources/file.txt");
}
});
@Test
public void testServletChange() throws InterruptedException {
RestAssured.when().get("/dev").then()
.statusCode(200)
.body(is("Hello World"));
test.modifySourceFile("DevServlet.java", new Function<String, String>() { (2)
@Override
public String apply(String s) {
return s.replace("Hello World", "Hello Quarkus");
}
});
RestAssured.when().get("/dev").then()
.statusCode(200)
.body(is("Hello Quarkus"));
}
@Test
public void testAddServlet() throws InterruptedException {
RestAssured.when().get("/new").then()
.statusCode(404);
test.addSourceFile(NewServlet.class); (3)
RestAssured.when().get("/new").then()
.statusCode(200)
.body(is("A new Servlet"));
}
@Test
public void testResourceChange() throws InterruptedException {
RestAssured.when().get("/file.txt").then()
.statusCode(200)
.body(is("Hello Resource"));
test.modifyResourceFile("META-INF/resources/file.txt", new Function<String, String>() { (4)
@Override
public String apply(String s) {
return "A new resource";
}
});
RestAssured.when().get("file.txt").then()
.statusCode(200)
.body(is("A new resource"));
}
@Test
public void testAddResource() throws InterruptedException {
RestAssured.when().get("/new.txt").then()
.statusCode(404);
test.addResourceFile("META-INF/resources/new.txt", "New File"); (5)
RestAssured.when().get("/new.txt").then()
.statusCode(200)
.body(is("New File"));
}
}
| 1 | これはデプロイを開始し、テストスイートの一部としてテストで変更を加えることができます。各テストメソッドの間に Quarkus が再起動されるため、すべてのメソッドはクリーンなデプロイで開始されます。 |
| 2 | このメソッドを使用すると、クラスファイルのソースを変更できます。古いソースが関数に渡され、更新されたソースが返されます。 |
| 3 | このメソッドは、デプロイに新しいクラスファイルを追加します。使用されるソースは、現在のプロジェクトに含まれるオリジナルのソースになります。 |
| 4 | このメソッドは静的リソースを変更します。 |
| 5 | このメソッドは新しい静的リソースを追加します。 |
2.17. ネイティブ実行ファイルのサポート
Quarkus は、ネイティブ実行ファイルのビルドの側面を制御する多くのビルドアイテムを提供しています。これにより、リフレクション用のクラスの登録や、ネイティブ実行ファイルへの静的リソースの追加などのタスクを、エクステンションからプログラムで実行できます。これらのビルドアイテムの一部を以下に示します。
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem-
静的リソースをネイティブ実行ファイルに含めます。
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem-
ディレクトリの静的リソースをネイティブ実行ファイルに含めます。
io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem-
実行時に Substrate によって再初期化されるクラス。これにより、静的初期化子が 2 回実行されることになります。
io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem-
ネイティブ実行ファイルのビルド時に設定されるシステムプロパティー。
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem-
リソースバンドルをネイティブ実行ファイルに含めます。
io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem-
Substrate でのリフレクション用にクラスを登録します。コンストラクターは常に登録されますが、メソッドとフィールドは任意です。
io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem-
ビルド時ではなく実行時に初期化されるクラス。クラスがネイティブ実行ファイルのビルドプロセスの一部として初期化されるとビルドが失敗するため、注意が必要です。
io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem-
上記の機能のほとんどを単一のビルドアイテムから制御できるようにする便利な機能。
io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem-
ネイティブイメージですべての文字セットを有効にすることを示します。
io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem-
エクステンションが SSL を必要とし、ネイティブイメージのビルド中に SSL を有効にする必要があることを Quarkus に伝えるための便利な方法です。この機能を使用する場合は、 native and ssl guide の SSL サポートを自動的に提供するエクステンションのリストに自分のエクステンションを追加することを忘れないでください。
2.18. IDE サポートのヒント
2.18.1. Eclipse での Quarkus エクステンションの作成
Eclipse で Quarkus エクステンションを書く際の唯一の特別な点は、エクステンションのビルドの一部として APT (Annotation Processing Tool) が必要であることです。つまり、以下を行う必要があります。
-
https://marketplace.eclipse.org/content/m2e-apt から
m2e-aptをインストールする。 -
pom.xmlで次のプロパティーを定義する:<m2e.apt.activation>jdt_apt</m2e.apt.activation>。ただし、io.quarkus:quarkus-build-parentに依存している場合は、デフォルトで設定されています。 -
IDE で
io.quarkus:quarkus-extension-processorプロジェクトを同時に開いている場合 (たとえば、Quarkus のソースをチェックアウトして IDE で開いている場合)、そのプロジェクトを閉じる必要があります。そうしないと、Eclipse はそこに含まれる APT プラグインを呼び出しません。 -
エクステンションプロセッサープロジェクトを閉じたばかりの場合は、Eclipse が Maven リポジトリーからエクステンションプロセッサーを取得できるように、他のプロジェクトで
Maven > Update Projectを必ず実行してください。
2.19. トラブルシューティングとデバッグのヒント
2.19.1. 生成/変換されたクラスの検査
Quarkus はビルドフェーズ中に多くのクラスを生成し、多くの場合、既存のクラスも変換します。エクステンションの開発中に、生成されたバイトコードや変換されたクラスを確認することは、非常に役立つことがよくあります。
quarkus.package.jar.decompiler.enabled プロパティーを true に設定すると、Quarkus は Vineflower デコンパイラー をダウンロードして呼び出し、その結果をビルドツールの出力の decompiled ディレクトリ (Maven の場合は target/decompiled など) に出力します。出力ディレクトリは quarkus.package.jar.decompiler.output-dir で変更できます。
このプロパティーは、通常の製品ビルド (つまり、開発モードやテストではない) で、かつ fast-jar パッケージングタイプが使用されている場合 (デフォルトの動作) にのみ機能します。
|
また、生成/変換されたクラスをファイルシステムに出力し、後で IDE のデコンパイラーなどで検査できるようにする 3 つのシステムプロパティーがあります。
-
quarkus.debug.generated-classes-dir- Bean メタデータなどの生成されたクラスを出力する -
quarkus.debug.transformed-classes-dir- Panache エンティティーなどの変換されたクラスを出力する -
quarkus.debug.generated-sources-dir- ZIG ファイルを出力する。ZIG ファイルは、スタックトレースで参照される生成されたコードのテキスト表現です。
これらのプロパティーは、開発モードや、生成/変換されたクラスがクラスローダーのメモリー内だけに保持されるテストの実行時に特に便利です。
たとえば、開発モードでこれらのクラスをディスクに書き出して検査するには、quarkus.debug.generated-classes-dir システムプロパティーを指定します。
./mvnw quarkus:dev -Dquarkus.debug.generated-classes-dir=dump-classes
プロパティー値は、Linux マシンの /home/foo/dump のような絶対パス、またはユーザーの作業ディレクトリからの相対パスのいずれかになります。つまり、dump は開発モードでは {user.dir}/target/dump に、テスト実行時は {user.dir}/dump に対応します。
|
ディレクトリに書き込まれた各クラスについて、ログに 1 行表示されるはずです。
INFO [io.qua.run.boo.StartupActionImpl] (main) Wrote /path/to/my/app/target/dump-classes/io/quarkus/arc/impl/ActivateRequestContextInterceptor_Bean.class
このプロパティーはテストの実行時にも尊重されます。
./mvnw clean test -Dquarkus.debug.generated-classes-dir=target/dump-generated-classes
同様に、quarkus.debug.transformed-classes-dir および quarkus.debug.generated-sources-dir プロパティーを使用して、関連する出力をダンプできます。
2.19.2. Inspecting Generated/Transformed Classes in QuarkusExtensionTest
When using QuarkusExtensionTest,
as an alternative to setting quarkus.debug.*-dir manually,
you may simply call QuarkusExtensionTest#debugBytecode:
public class MyTest {
@RegisterExtension
static QuarkusExtensionTest runner = new QuarkusExtensionTest()
.withApplicationRoot((jar) -> jar
.addClass(MyEntity.class))
.debugBytecode(true);
// ... test methods go here ...
}
This will automatically set up these configuration properties so that classes/sources
are dumped to target/debug, for that test class only,
in a subdirectory that is unique to each test execution.
See the javadoc of QuarkusExtensionTest#debugBytecode for details.
これは特に、 CI 環境でのみ発生する不安定なテストをデバッグするのに便利です。たとえば、 https://github.com/quarkusio/quarkus/ の GitHub Actions CI は、各 CI 実行後にダウンロード可能なビルドアティファクトとして、そのような target/debug ディレクトリーが収集されるように設定されています。
2.19.3. 特定のテストのみでトレースログを有効化する方法
When using QuarkusExtensionTest,
if you need to enable trace logs for a particular test class,
you may simply call QuarkusExtensionTest#traceCategories and pass the logging categories in argument:
public class MyTest {
@RegisterExtension
static QuarkusExtensionTest runner = new QuarkusExtensionTest()
.withApplicationRoot((jar) -> jar
.addClass(MyEntity.class))
.traceCategories("org.hibernate", "io.quarkus.hibernate", "io.quarkus.panache");
// ... test methods go here ...
}
See the javadoc of QuarkusExtensionTest#traceCategories for details.
これは、オプションを有効にした特定のテストでのみログの冗長性が高まるため、特に CI 環境でのみ発生する不安定なテストをデバッグするのに便利です。
2.20. サンプルテストエクステンション
エクステンション処理のデグレードをテストするために使用されるエクステンションがあります。これは https://github.com/quarkusio/quarkus/tree/main/integration-tests/test-extension/extension ディレクトリーにあります。このセクションでは、タスクがどのように実行できるかを説明するために、テストエクステンションのコードを使用して、エクステンションの作成者が通常実行する必要があるいくつかのタスクについて触れます。
2.20.1. Feature と Capability
2.20.1.1. Feature
feature は、エクステンションによって提供される機能を表します。Feature の名前は、アプリケーションのブートストラップ中にログに表示されます。
2019-03-22 14:02:37,884 INFO [io.quarkus] (main) Quarkus 999-SNAPSHOT started in 0.061s.
2019-03-22 14:02:37,884 INFO [io.quarkus] (main) Installed features: [cdi, test-extension] (1)
| 1 | ランタイムイメージにインストールされている Feature のリスト |
Feature は、FeatureBuildItem を生成する ビルドステッププロセッサー メソッドに登録できます。
@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem("test-extension");
}
Feature 名には小文字のみを使用し、単語はダッシュで区切る必要があります (例: security-jpa)。エクステンションが提供する Feature は最大 1 つとし、名前は一意である必要があります。複数のエクステンションが同じ名前の Feature を登録すると、ビルドは失敗します。
Feature 名は、エクステンションの devtools/common/src/main/filtered/extensions.json エントリ内のラベルともマッピングされている必要があります。これにより、スタートアップ行に表示される Feature 名が、Quarkus Maven プラグインを使用してプロジェクトを作成するときにエクステンションを選択するために使用できるラベルと一致します。これは JSON REST サービスの作成 ガイドから引用した例で、rest-jackson Feature が参照されています。
mvn io.quarkus.platform:quarkus-maven-plugin:3.35.2:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=rest-json \
-DclassName="org.acme.rest.json.FruitResource" \
-Dpath="/fruits" \
-Dextensions="rest,rest-jackson"
cd rest-json
2.20.1.2. Capability
capability は、他のエクステンションから問い合わせることができる技術的な能力を表します。エクステンションは複数の Capability を提供でき、複数のエクステンションが同じ Capability を提供することもできます。デフォルトでは、Capability はユーザーには表示されません。エクステンションの存在を確認する場合は、クラスパスベースのチェックではなく、Capability を使用する必要があります。
Capability は、CapabilityBuildItem を生成する ビルドステッププロセッサー メソッドに登録できます。
@BuildStep
void capabilities(BuildProducer<CapabilityBuildItem> capabilityProducer) {
capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-transactions"));
capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-metrics"));
}
エクステンションは、Capabilities ビルドアイテムを使用して、登録された Capability を利用できます。
@BuildStep
void doSomeCoolStuff(Capabilities capabilities) {
if (capabilities.isPresent(Capability.TRANSACTIONS)) {
// do something only if JTA transactions are in...
}
}
Capability は、Java パッケージの命名規則に従う必要があります (例: io.quarkus.security.jpa)。コアエクステンションによって提供される Capability は、io.quarkus.deployment.Capability 列挙型にリストされる必要があり、その名前は常に io.quarkus プレフィックスで始まる必要があります。
2.20.2. Bean 定義アノテーション
CDI レイヤーは、明示的に登録された CDI Bean、または 2.5.1. Bean 定義アノテーション で定義されている Bean 定義アノテーションに基づいて発見された CDI Bean を処理します。この TestProcessor#registerBeanDefinningAnnotations の例に示すように、BeanDefiningAnnotationBuildItem を使用して、エクステンションが処理するアノテーションを含むようにこのアノテーションのセットを拡張できます。
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.jandex.DotName;
import io.quarkus.extest.runtime.TestAnnotation;
public final class TestProcessor {
static DotName TEST_ANNOTATION = DotName.createSimple(TestAnnotation.class.getName());
static DotName TEST_ANNOTATION_SCOPE = DotName.createSimple(ApplicationScoped.class.getName());
...
@BuildStep
BeanDefiningAnnotationBuildItem registerX() {
(1)
return new BeanDefiningAnnotationBuildItem(TEST_ANNOTATION, TEST_ANNOTATION_SCOPE);
}
...
}
/**
* Marker annotation for test configuration target beans
*/
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
@Inherited
public @interface TestAnnotation {
}
/**
* A sample bean
*/
@TestAnnotation (2)
public class ConfiguredBean implements IConfigConsumer {
...
| 1 | Jandex の DotName クラスを使用して、アノテーションクラスと CDI デフォルトスコープを登録します。 |
| 2 | ConfiguredBean は、CDI 標準の @ApplicationScoped でアノテーションされた Bean と同様に CDI レイヤーによって処理されます。 |
2.20.3. 設定のオブジェクトへの解析
エクステンションが行う可能性が高い主なことの 1 つは、動作の設定フェーズをランタイムフェーズから完全に分離することです。フレームワークは起動時に設定の解析/ロードを行うことが多いですが、これをビルド時に行うことで、XML 解析ツールなどのフレームワークへの実行時の依存関係を減らすとともに、解析にかかる起動時間を短縮できます。
JAXB を使用して XML 設定ファイルを解析する例が、TestProcessor#parseServiceXmlConfig メソッドに示されています。
@BuildStep
@Record(STATIC_INIT)
RuntimeServiceBuildItem parseServiceXmlConfig(TestRecorder recorder) throws JAXBException {
RuntimeServiceBuildItem serviceBuildItem = null;
JAXBContext context = JAXBContext.newInstance(XmlConfig.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream("/config.xml"); (1)
if (is != null) {
log.info("Have XmlConfig, loading");
XmlConfig config = (XmlConfig) unmarshaller.unmarshal(is); (2)
...
}
return serviceBuildItem;
}
| 1 | config.xml クラスパスリソースを探します。 |
| 2 | 見つかった場合は、XmlConfig.class の JAXB コンテキストを使用して解析します。 |
|
ビルド環境で /config.xml リソースが利用可能でなかった場合、null の |
通常、parseServiceXmlConfig が行っているように、ランタイムコンポーネント/サービスを作成するために設定をロードします。parseServiceXmlConfig における残りの動作については、後の 非 CDI サービスの管理 セクションで説明します。
何らかの理由で設定を解析し、エクステンションプロセッサーの他のビルドステップで使用する必要がある場合は、解析された XmlConfig インスタンスを渡すための XmlConfigBuildItem を作成する必要があります。
|
XmlConfig のコードを見ると、JAXB アノテーションが付いていることがわかります。これらをランタイムイメージに含めたくない場合は、XmlConfig インスタンスを何らかの POJO オブジェクトグラフに複製し、XmlConfig を POJO クラスに置き換えることができます。これについては、ネイティブイメージでのクラスの置き換え で行います。 |
2.20.4. Jandex を使用したデプロイメントのスキャン
エクステンションが、処理が必要な Bean をマークするアノテーションやインターフェイスを定義している場合、Java アノテーションインデクサーおよびオフラインリフレクションライブラリーである Jandex API を使用して、これらの Bean を見つけることができます。次の TestProcessor#scanForBeans メソッドは、@TestAnnotation が付与され、かつ IConfigConsumer インターフェイスを実装している Bean を見つける方法を示しています。
static DotName TEST_ANNOTATION = DotName.createSimple(TestAnnotation.class.getName());
...
@BuildStep
@Record(STATIC_INIT)
void scanForBeans(TestRecorder recorder, BeanArchiveIndexBuildItem beanArchiveIndex, (1)
BuildProducer<TestBeanBuildItem> testBeanProducer) {
IndexView indexView = beanArchiveIndex.getIndex(); (2)
Collection<AnnotationInstance> testBeans = indexView.getAnnotations(TEST_ANNOTATION); (3)
for (AnnotationInstance ann : testBeans) {
ClassInfo beanClassInfo = ann.target().asClass();
try {
boolean isConfigConsumer = beanClassInfo.interfaceNames()
.stream()
.anyMatch(dotName -> dotName.equals(DotName.createSimple(IConfigConsumer.class.getName()))); (4)
if (isConfigConsumer) {
Class<IConfigConsumer> beanClass = (Class<IConfigConsumer>) Class.forName(beanClassInfo.name().toString(), false, Thread.currentThread().getContextClassLoader());
testBeanProducer.produce(new TestBeanBuildItem(beanClass)); (5)
log.infof("Configured bean: %s", beanClass);
}
} catch (ClassNotFoundException e) {
log.warn("Failed to load bean class", e);
}
}
}
| 1 | デプロイメントのインデックス作成後にビルドステップが実行されるように、BeanArchiveIndexBuildItem に依存させます。 |
| 2 | インデックスを取得します。 |
| 3 | @TestAnnotation でアノテーションされたすべての Bean を探します。 |
| 4 | これらの Bean のうち、どれが IConfigConsumer インターフェイスも持っているかを判断します。 |
| 5 | Bean インスタンスとやり取りする後の RUNTIME_INIT ビルドステップで使用するために、Bean クラスを TestBeanBuildItem に保存します。 |
2.20.5. エクステンション Bean との相互作用
io.quarkus.arc.runtime.BeanContainer インターフェイスを使用して、エクステンション Bean とやり取りできます。次の configureBeans メソッドは、前のセクションでスキャンされた Bean とのやり取りを示しています。
// TestProcessor#configureBeans
@BuildStep
@Record(RUNTIME_INIT)
void configureBeans(TestRecorder recorder, List<TestBeanBuildItem> testBeans, (1)
BeanContainerBuildItem beanContainer, (2)
TestRunTimeConfig runTimeConfig) {
for (TestBeanBuildItem testBeanBuildItem : testBeans) {
Class<IConfigConsumer> beanClass = testBeanBuildItem.getConfigConsumer();
recorder.configureBeans(beanContainer.getValue(), beanClass, buildAndRunTimeConfig, runTimeConfig); (3)
}
}
// TestRecorder#configureBeans
public void configureBeans(BeanContainer beanContainer, Class<IConfigConsumer> beanClass,
TestBuildAndRunTimeConfig buildTimeConfig,
TestRunTimeConfig runTimeConfig) {
log.info("Begin BeanContainerListener callback\n");
IConfigConsumer instance = beanContainer.beanInstance(beanClass); (4)
instance.loadConfig(buildTimeConfig, runTimeConfig); (5)
log.infof("configureBeans, instance=%s\n", instance);
}
| 1 | スキャンビルドステップから生成された TestBeanBuildItem を利用します。 |
| 2 | BeanContainerBuildItem を利用して、CDI Bean コンテナーが作成された後にこのビルドステップが実行されるように順序付けます。 |
| 3 | ランタイムレコーダーを呼び出して、Bean とのやり取りを記録します。 |
| 4 | ランタイムレコーダーは、その型を使用して Bean を取得します。 |
| 5 | ランタイムレコーダーは、ランタイム情報を含む設定オブジェクトを渡して IConfigConsumer#loadConfig(…) メソッドを呼び出します。 |
2.20.6. 非 CDI サービスの管理
エクステンションの一般的な目的は、CDI を認識しないサービスを CDI ベースの Quarkus ランタイムに統合することです。このタスクのステップ 1 は、設定のオブジェクトへの解析 で行ったように、STATIC_INIT ビルドステップで必要な設定をロードすることです。次に、設定を使用してサービスのインスタンスを作成する必要があります。TestProcessor#parseServiceXmlConfig メソッドに戻って、これがどのように行われるかを見てみましょう。
// TestProcessor#parseServiceXmlConfig
@BuildStep
@Record(STATIC_INIT)
RuntimeServiceBuildItem parseServiceXmlConfig(TestRecorder recorder) throws JAXBException {
RuntimeServiceBuildItem serviceBuildItem = null;
JAXBContext context = JAXBContext.newInstance(XmlConfig.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream("/config.xml");
if (is != null) {
log.info("Have XmlConfig, loading");
XmlConfig config = (XmlConfig) unmarshaller.unmarshal(is);
log.info("Loaded XmlConfig, creating service");
RuntimeValue<RuntimeXmlConfigService> service = recorder.initRuntimeService(config); (1)
serviceBuildItem = new RuntimeServiceBuildItem(service); (3)
}
return serviceBuildItem;
}
// TestRecorder#initRuntimeService
public RuntimeValue<RuntimeXmlConfigService> initRuntimeService(XmlConfig config) {
RuntimeXmlConfigService service = new RuntimeXmlConfigService(config); (2)
return new RuntimeValue<>(service);
}
// RuntimeServiceBuildItem
final public class RuntimeServiceBuildItem extends SimpleBuildItem {
private RuntimeValue<RuntimeXmlConfigService> service;
public RuntimeServiceBuildItem(RuntimeValue<RuntimeXmlConfigService> service) {
this.service = service;
}
public RuntimeValue<RuntimeXmlConfigService> getService() {
return service;
}
}
| 1 | ランタイムレコーダーを呼び出して、サービスの作成を記録します。 |
| 2 | 解析された XmlConfig インスタンスを使用して RuntimeXmlConfigService のインスタンスを作成し、それを RuntimeValue でラップします。プロキシ不可能な非インターフェイスオブジェクトには RuntimeValue ラッパーを使用します。 |
| 3 | サービスを開始する RUNTIME_INIT ビルドステップで使用するために、戻り値のサービス値を RuntimeServiceBuildItem でラップします。 |
2.20.6.1. サービスの開始
ビルドフェーズ中にサービスの作成を記録したので、ブート中の実行時にサービスを開始する方法を記録する必要があります。これを行うには、TestProcessor#startRuntimeService メソッドに示されているように、RUNTIME_INIT ビルドステップを使用します。
// TestProcessor#startRuntimeService
@BuildStep
@Record(RUNTIME_INIT)
ServiceStartBuildItem startRuntimeService(TestRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem , (1)
RuntimeServiceBuildItem serviceBuildItem) throws IOException { (2)
if (serviceBuildItem != null) {
log.info("Registering service start");
recorder.startRuntimeService(shutdownContextBuildItem, serviceBuildItem.getService()); (3)
} else {
log.info("No RuntimeServiceBuildItem seen, check config.xml");
}
return new ServiceStartBuildItem("RuntimeXmlConfigService"); (4)
}
// TestRecorder#startRuntimeService
public void startRuntimeService(ShutdownContext shutdownContext, RuntimeValue<RuntimeXmlConfigService> runtimeValue)
throws IOException {
RuntimeXmlConfigService service = runtimeValue.getValue();
service.startService(); (5)
shutdownContext.addShutdownTask(service::stopService); (6)
}
| 1 | サービスのシャットダウンを登録するために ShutdownContextBuildItem を利用します。 |
| 2 | RuntimeServiceBuildItem でキャプチャーされた、以前に初期化済みのサービスを利用します。 |
| 3 | ランタイムレコーダーを呼び出し、サービスの開始呼び出しを記録します。 |
| 4 | サービスの起動を示す ServiceStartBuildItem を生成します。詳細は 起動およびシャットダウンイベント を参照してください。 |
| 5 | ランタイムレコーダーがサービスインスタンスの参照を取得し、その startService メソッドを呼び出します。 |
| 6 | ランタイムレコーダーが、サービスインスタンスの stopService メソッドの呼び出しを Quarkus の ShutdownContext に登録します。 |
RuntimeXmlConfigService のコードはこちらで確認できます。 RuntimeXmlConfigService.java
RuntimeXmlConfigService が起動したことを検証するテストケースは、 ConfiguredBeanTest および NativeImageIT の testRuntimeXmlConfigService テストにあります。
2.20.7. 起動およびシャットダウンイベント
Quarkus コンテナーは、コンテナーの起動とシャットダウンをコンポーネントに通知するための起動およびシャットダウンライフサイクルイベントをサポートしています。この例では、コンポーネントが監視できる発行済みの CDI イベントを示しています。
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
public class SomeBean {
/**
* Called when the runtime has started
* @param event
*/
void onStart(@Observes StartupEvent event) { (1)
System.out.printf("onStart, event=%s%n", event);
}
/**
* Called when the runtime is shutting down
* @param event
*/
void onStop(@Observes ShutdownEvent event) { (2)
System.out.printf("onStop, event=%s%n", event);
}
}
| 1 | ランタイムが起動したことの通知を受け取るには、 StartupEvent を監視します。 |
| 2 | ランタイムがシャットダウンされる際の通知を受け取るには、 ShutdownEvent を監視します。 |
エクステンションの開発者にとって、起動およびシャットダウンイベントにはどのような関連があるのでしょうか。 サービスの開始 セクションで、シャットダウンタスクを実行するコールバックを登録するために ShutdownContext を使用する例をすでに確認しました。これらのシャットダウンタスクは、 ShutdownEvent が送信された後に呼び出されます。
StartupEvent は、すべての io.quarkus.deployment.builditem.ServiceStartBuildItem プロデューサーが消費された後に発行されます。これが意味するのは、あるエクステンションに、アプリケーションコンポーネントが StartupEvent を監視するときに起動済みであることを期待するサービスがある場合、それらのサービスを起動するランタイムコードを呼び出すビルドステップは、 StartupEvent が送信される前にランタイムコードが実行されることを保証するために ServiceStartBuildItem を生成する必要があるということです。前のセクションで ServiceStartBuildItem の生成を確認しましたが、わかりやすくするためにここでも繰り返します。
// TestProcessor#startRuntimeService
@BuildStep
@Record(RUNTIME_INIT)
ServiceStartBuildItem startRuntimeService(TestRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem,
RuntimeServiceBuildItem serviceBuildItem) throws IOException {
...
return new ServiceStartBuildItem("RuntimeXmlConfigService"); (1)
}
| 1 | StartupEvent が送信される前に実行する必要があるサービス起動ステップであることを示すために、 ServiceStartBuildItem を生成します。 |
2.20.8. ネイティブイメージで使用するリソースの登録
すべての設定やリソースをビルド時に消費できるわけではありません。ランタイムでアクセスする必要があるクラスパスリソースがある場合、それらのリソースをネイティブイメージにコピーする必要があることをビルドフェーズに通知する必要があります。これは、1 つ以上の NativeImageResourceBuildItem を生成するか、リソースバンドルの場合は NativeImageResourceBundleBuildItem を生成することによって行います。この例を、以下の registerNativeImageResources ビルドステップに示します。
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<NativeImageResourceBuildItem> resource, BuildProducer<NativeImageResourceBundleBuildItem> resourceBundle) {
resource.produce(new NativeImageResourceBuildItem("/security/runtime.keys")); (1)
resource.produce(new NativeImageResourceBuildItem(
"META-INF/my-descriptor.xml")); (2)
resourceBundle.produce(new NativeImageResourceBuildItem("jakarta.xml.bind.Messages")); (3)
}
}
| 1 | /security/runtime.keys クラスパスリソースをネイティブイメージにコピーすることを指定します。 |
| 2 | META-INF/my-descriptor.xml リソースをネイティブイメージにコピーすることを指定します。 |
| 3 | "jakarta.xml.bind.Messages" リソースバンドルをネイティブイメージにコピーすることを指定します。 |
2.20.9. サービスファイル
META-INF/services ファイルを使用している場合は、ネイティブイメージがそれらを見つけられるようにリソースとして登録する必要があります。また、実行時にインスタンス化や検査ができるように、リストされている各クラスをリフレクション用に登録する必要もあります。
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<ServiceProviderBuildItem> services) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// find out all the implementation classes listed in the service files
Set<String> implementations =
ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
// register every listed implementation class so they can be instantiated
// in native-image at run-time
services.produce(
new ServiceProviderBuildItem(io.quarkus.SomeService.class.getName(),
implementations.toArray(new String[0])));
}
}
ServiceProviderBuildItem はサービス実装クラスのリストをパラメーターとして受け取ります。サービスファイルから読み込まない場合は、実行時にもサービスファイルが読み込まれて使用されるため、それらがサービスファイルの内容と一致していることを確認してください。これはサービスファイルを作成することの代わりにはなりません。
|
| これはリフレクションを介したインスタンス化のために実装クラスを登録するだけです (フィールドやメソッドを検査することはできません)。それらを行う必要がある場合は、次のように記述できます。 |
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<NativeImageResourceBuildItem> resource,
BuildProducer<ReflectiveClassBuildItem> reflectionClasses) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// register the service file so it is visible in native-image
resource.produce(new NativeImageResourceBuildItem(service));
// register every listed implementation class so they can be inspected/instantiated
// in native-image at run-time
Set<String> implementations =
ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
reflectionClasses.produce(
new ReflectiveClassBuildItem(true, true, implementations.toArray(new String[0])));
}
}
これはサービスをネイティブで動作させる最も簡単な方法ですが、ビルド時に実装クラスをスキャンし、リフレクションに頼らずに静的初期化時にそれらを登録するコードを生成するよりも効率が劣ります。
前のビルドステップを、クラスをリフレクション用に登録する代わりに static-init レコーダーを使用するように変更することで、効率化を実現できます。
public final class MyExtProcessor {
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void registerNativeImageResources(RecorderContext recorderContext,
SomeServiceRecorder recorder) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// read the implementation classes
Collection<Class<? extends io.quarkus.SomeService>> implementationClasses = new LinkedHashSet<>();
Set<String> implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
for(String implementation : implementations) {
implementationClasses.add((Class<? extends io.quarkus.SomeService>)
recorderContext.classProxy(implementation));
}
// produce a static-initializer with those classes
recorder.configure(implementationClasses);
}
}
@Recorder
public class SomeServiceRecorder {
public void configure(List<Class<? extends io.quarkus.SomeService>> implementations) {
// configure our service statically
SomeServiceProvider serviceProvider = SomeServiceProvider.instance();
SomeServiceBuilder builder = serviceProvider.getSomeServiceBuilder();
List<io.quarkus.SomeService> services = new ArrayList<>(implementations.size());
// instantiate the service implementations
for (Class<? extends io.quarkus.SomeService> implementationClass : implementations) {
try {
services.add(implementationClass.getConstructor().newInstance());
} catch (Exception e) {
throw new IllegalArgumentException("Unable to instantiate service " + implementationClass, e);
}
}
// build our service
builder.withSomeServices(implementations.toArray(new io.quarkus.SomeService[0]));
ServiceManager serviceManager = builder.build();
// register it
serviceProvider.registerServiceManager(serviceManager, Thread.currentThread().getContextClassLoader());
}
}
2.20.10. オブジェクトの置換
ビルドフェーズで作成され、ランタイムに渡されるオブジェクトは、ビルド時の状態からランタイムの起動時に作成および構成されるために、デフォルトコンストラクターを持つ必要があります。オブジェクトにデフォルトコンストラクターがない場合、オーグメンテーションされたアーティファクトの生成中に次のようなエラーが表示されます。
[error]: Build step io.quarkus.deployment.steps.MainClassBuildStep#build threw an exception: java.lang.RuntimeException: Unable to serialize objects of type class sun.security.provider.DSAPublicKeyImpl to bytecode as it has no default constructor
at io.quarkus.builder.Execution.run(Execution.java:123)
at io.quarkus.builder.BuildExecutionBuilder.execute(BuildExecutionBuilder.java:136)
at io.quarkus.deployment.QuarkusAugmentor.run(QuarkusAugmentor.java:110)
at io.quarkus.runner.RuntimeRunner.run(RuntimeRunner.java:99)
... 36 more
このようなクラスを Quarkus がどのように処理するかを指定するために実装できる io.quarkus.runtime.ObjectSubstitution インターフェースがあります。 DSAPublicKey に対する実装例を以下に示します。
package io.quarkus.extest.runtime.subst;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.DSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.logging.Logger;
import io.quarkus.runtime.ObjectSubstitution;
public class DSAPublicKeyObjectSubstitution implements ObjectSubstitution<DSAPublicKey, KeyProxy> {
private static final Logger log = Logger.getLogger("DSAPublicKeyObjectSubstitution");
@Override
public KeyProxy serialize(DSAPublicKey obj) { (1)
log.info("DSAPublicKeyObjectSubstitution.serialize");
byte[] encoded = obj.getEncoded();
KeyProxy proxy = new KeyProxy();
proxy.setContent(encoded);
return proxy;
}
@Override
public DSAPublicKey deserialize(KeyProxy obj) { (2)
log.info("DSAPublicKeyObjectSubstitution.deserialize");
byte[] encoded = obj.getContent();
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encoded);
DSAPublicKey dsaPublicKey = null;
try {
KeyFactory kf = KeyFactory.getInstance("DSA");
dsaPublicKey = (DSAPublicKey) kf.generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
}
return dsaPublicKey;
}
}
| 1 | serialize メソッドは、デフォルトコンストラクターを持たないオブジェクトを受け取り、 DSAPublicKey を再作成するために必要な情報を含む KeyProxy を作成します。 |
| 2 | deserialize メソッドは、キーファクトリーを使用して、エンコードされた形式の KeyProxy から DSAPublicKey を再作成します。 |
エクステンションは、この TestProcessor#loadDSAPublicKey の断片に示されているように、 ObjectSubstitutionBuildItem を生成することでこの置換を登録します。
@BuildStep
@Record(STATIC_INIT)
PublicKeyBuildItem loadDSAPublicKey(TestRecorder recorder,
BuildProducer<ObjectSubstitutionBuildItem> substitutions) throws IOException, GeneralSecurityException {
...
// Register how to serialize DSAPublicKey
ObjectSubstitutionBuildItem.Holder<DSAPublicKey, KeyProxy> holder = new ObjectSubstitutionBuildItem.Holder(
DSAPublicKey.class, KeyProxy.class, DSAPublicKeyObjectSubstitution.class);
ObjectSubstitutionBuildItem keysub = new ObjectSubstitutionBuildItem(holder);
substitutions.produce(keysub);
log.info("loadDSAPublicKey run");
return new PublicKeyBuildItem(publicKey);
}
2.20.11. ネイティブイメージでのクラスの置き換え
Graal SDK はネイティブイメージでのクラスの置換をサポートしています。 XmlConfig/XmlData クラスを、JAXB アノテーションへの依存関係がないバージョンに置き換える方法の例を、以下のサンプルクラスに示します。
package io.quarkus.extest.runtime.graal;
import java.util.Date;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import io.quarkus.extest.runtime.config.XmlData;
@TargetClass(XmlConfig.class)
@Substitute
public final class Target_XmlConfig {
@Substitute
private String address;
@Substitute
private int port;
@Substitute
private ArrayList<XData> dataList;
@Substitute
public String getAddress() {
return address;
}
@Substitute
public int getPort() {
return port;
}
@Substitute
public ArrayList<XData> getDataList() {
return dataList;
}
@Substitute
@Override
public String toString() {
return "Target_XmlConfig{" +
"address='" + address + '\'' +
", port=" + port +
", dataList=" + dataList +
'}';
}
}
@TargetClass(XmlData.class)
@Substitute
public final class Target_XmlData {
@Substitute
private String name;
@Substitute
private String model;
@Substitute
private Date date;
@Substitute
public String getName() {
return name;
}
@Substitute
public String getModel() {
return model;
}
@Substitute
public Date getDate() {
return date;
}
@Substitute
@Override
public String toString() {
return "Target_XmlData{" +
"name='" + name + '\'' +
", model='" + model + '\'' +
", date='" + date + '\'' +
'}';
}
}
3. エコシステムの統合
プライベートなエクステンションもあれば、より広範な Quarkus エコシステムの一部となり、 コミュニティーでの再利用を可能に したいものもあるでしょう。 Quarkiverse Hub への掲載は、継続的なテストと公開を処理するための便利なメカニズムです。 Quarkiverse Hub wiki には、エクステンションをオンボーディングするための手順が記載されています。
あるいは、継続的なテストと公開を手動で処理することも可能です。
3.2. registry.quarkus.io でのエクステンションの公開
Quarkus ツール でエクステンションを公開する前に、以下の要件が満たされていることを確認してください。
-
quarkus-extension.yaml ファイル (エクステンションの
runtime/モジュール内) に、最小限のメタデータが設定されていること。-
name -
description(推奨される方法であるruntime/pom.xmlの<description>要素にすでに設定されている場合を除く)
-
-
エクステンションが Maven Central で公開されていること
-
エクステンションのリポジトリーが エコシステム CI を使用するように構成されていること。
次に、 Quarkus Extension Catalog の extensions/ ディレクトリーに your-extension.yaml ファイルを追加するプルリクエストを作成する必要があります。YAML は次の構造である必要があります。
group-id: <YOUR_EXTENSION_RUNTIME_GROUP_ID>
artifact-id: <YOUR_EXTENSION_RUNTIME_ARTIFACT_ID>
| リポジトリーに複数のエクステンションが含まれている場合は、リポジトリー全体に対して 1 つのファイルを作成するのではなく、個々のエクステンションごとに個別のファイルを作成する必要があります。 |
以上です。プルリクエストがマージされると、スケジュールされたジョブが Maven Central で新しいバージョンをチェックし、 Quarkus Extension Registry を更新します。