The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

独自のエクステンションの作成

Quarkusのエクステンションは、コアサービスに開発者向けの新しい動作を追加するもので、ビルド時の拡張とランタイムコンテナという2つの異なる部分から構成されています。拡張部分は、アノテーションやXML記述子の読み込みなど、すべてのメタデータ処理を担当します。この拡張フェーズの出力は、関連するランタイムサービスを直接インスタンス化するためのバイトコードとして記録されます。

これは、メタデータがビルド時に一度だけ処理されることを意味し、起動時間の節約と、処理に使用されるクラスなどがランタイムJVMにロードされない(あるいは存在しない)ため、メモリ使用量の節約の両方を実現しています。

これは詳細なドキュメントです。入門書が必要な場合は、 初めてのエクステンションの作成をご覧ください。

1. エクステンション哲学

このセクションは作業中であり、エクステンションがどのように設計され、どのように書かれるべきかの哲学を記述します。

1.1. なぜエクステンションフレームワークなのか

Quarkusの使命は、使用するライブラリを含むアプリケーション全体を、従来のアプローチよりも大幅に少ないリソースしか使用しないアーティファクトに変換することです。これらを使用して、GraalVMを使用してネイティブアプリケーションを構築することができます。これを行うためには、アプリケーションの完全な「クローズドワールド」を分析し、理解する必要があります。完全で完璧なコンテキストがなければ、達成可能なものは最高でも部分的で限定的、一般的なサポートです。Quarkusのエクステンションアプローチを使用することで、Kubernetesやクラウドプラットフォームのようなメモリフットプリントに制約のある環境にJavaアプリケーションを合わせることができます。

Quarkusエクステンションフレームワークは、GraalVMを使用していない場合(HotSpotなど)でも、リソース利用率を大幅に改善します。エクステンションが実行するアクションをリストアップしてみましょう:

  • ビルド時のメタデータを収集し、コードを生成

    • この部分はGraalVMとは何の関係もありませんが、Quarkusがフレームワークを"ビルド時" に起動する方法です。

    • エクステンションフレームワークは、必要に応じてメタデータの読み込み、クラスのスキャン、クラスの生成を容易にします。

    • 拡張作業のごく一部は生成されたクラスを介して実行時に実行され、作業の大部分はビルド時に行われます (デプロイメント時と呼ばれます)

  • アプリケーションの近い世界観に基づいて、定見に基づいた賢明なデフォルトを強制(例えば、 @Entity のないアプリケーションは、Hibernate ORM を起動する必要はありません)

  • エクステンションは Substrate VM のコード置換をホストし、ライブラリを GraalVM 上で実行できるようにします。

    • ほとんどの変更は、基礎となるライブラリが GraalVM 上で動作するように upstream にプッシュされます。

    • すべての変更をupstreamにプッシュできるわけではないので、エクステンションは Substrate VM 置換をホスト。これはコードパッチの一形態で、ライブラリが実行できるようになっています。

  • Substrate VMコード置換のホストにより、アプリケーションのニーズに基づいたデッドコードの排除を支援します。

    • これはアプリケーションに依存しており、ライブラリ自体で共有することはできません。

    • たとえば、Quarkusは、特定の接続プールとキャッシュプロバイダだけが必要であることを知っているため、Hibernateコードを最適化します。

  • メタデータをGraalVMに送信。例えば、リフレクションに必要なクラス

    • この情報はライブラリ(Hibernateなど)ごとに静的ではありませんが、フレームワークはセマンティックな知識を持っており、どのクラスがリフレクションを必要とするかを知っています(例えば@Entityクラスなど)。

1.2. ランタイムワークよりもビルドタイムワーク

可能な限り、フレームワークに起動時(ランタイム)に作業をさせるのではなく、ビルド時(エクステンションのデプロイメント部分)に作業を行うことをお勧めします。そこでの作業が多いほど、そのエクステンションを使用しているQuarkusアプリケーションは小さくなり、ロードが速くなります。

1.3. 設定を公開する方法

Quarkusは、最も一般的な使用法を簡略化しています。つまり、そのデフォルトは、統合されているライブラリとは異なる場合があります。

シンプルな体験を最も簡単にするために、SmallRye Configを介して application.properties で設定を統一します。ライブラリ固有の設定ファイルは避けるか、少なくともオプションにしてください。例えば、Hibernate ORM用の persistence.xml はオプションです。

エクステンションは、ライブラリの体験に焦点を当てるのではなく、Quarkusアプリケーションとして全体的に設定を見るべきです。例えば、データベースアクセスの定義が共有タスクであるように、 quarkus.database.url 等々がエクステンション間で共有されます(例えば hibernate. プロパティを使用される代わりに)。最も便利な設定オプションは、ライブラリの自然な名前空間ではなく、 quarkus.[extension]. として公開されるべきです。あまり一般的ではないプロパティは、ライブラリの名前空間に置くことができます。

Quarkusが最適化できる閉じた世界の仮定を完全に有効にするには、ビルド時に設定された設定オプションと実行時にオーバーライド可能な設定オプションのどちらを採用するか検討するべきでしょう。もちろん、ホスト、ポート、パスワードなどのプロパティは、実行時にオーバーライド可能でなければなりません。しかし、キャッシングを有効にしたり、JDBCドライバを設定したりするような多くのプロパティは、アプリケーションの再構築を安全に要求することができます。

1.3.1. スタティック初期化設定

If the extension provides additional Config Sources and if these are required during Static Init, these must be registered with StaticInitConfigBuilderBuildItem. Configuration in Static Init does not scan for additional sources to avoid double initialization at application startup time.

1.4. CDI でコンポーネントを公開する

CDI がコンポーネントの構成に関して中心的なプログラミングモデルであるため、フレームワークやエクステンションはそのコンポーネントを、ユーザアプリケーションが容易に消費できるBeanとして公開しなければなりません。例えば、Hibernate ORM は EntityManagerFactoryEntityManager の Bean を公開し、コネクションプールは DataSource のBeanを公開します。エクステンションは、ビルド時にこれらのBean定義を登録しなければなりません。

1.4.1. クラスに裏付けられたBean

エクステンションは AdditionalBeanBuildItem を生成して、元のアプリケーションの一部であるかのようにクラスからビーン定義を読み取るようにコンテナに指示することができます。

登録されている Bean クラス AdditionalBeanBuildItem
@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 を注入することができます。

エクステンション 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インスタンスを取得することは 違反 です。理由は、CDIコンテナがまだ起動していないからです。CDIコンテナは Static initブートストラップフェーズ の間に起動されています。

BUILD_AND_RUN_TIME_FIXEDRUN_TIME 設定ルートは、どのBeanにも注入することができます。 RUN_TIME 設定ルールは、ブートストラップの後にのみ注入すべきです。

しかし、 recorderメソッド からBeanメソッドを呼び出すことは可能です。 @Record(STATIC_INIT) ビルドステップで Bean にアクセスする必要がある場合は、 BeanContainerBuildItem に依存するか、 BeanContainerListenerBuildItem でロジックをラップしなければなりません。理由は簡単で、CDIコンテナが完全に初期化されて起動していることを確認する必要があるからです。しかし、CDI コンテナは @Record(RUNTIME_INIT) ビルドステップで完全に初期化されて実行されていると思っておいた方が安全です。コンテナへの参照は、 CDI.current() またはQuarkus固有の Arc.container() .

Beanの状態が可視性を保証していることを担保することを忘れないでください。たとえば、volatile キーワードです。
この「遅延初期化」アプローチには、1つの重大な欠点があります。 初期化されていない Beanは、ブートストラップ中にインスタンス化された他のエクステンションやアプリケーションコンポーネントからアクセスされる可能性があります。 synthetic_beans] で、よりロバストな解決策を取り上げます。

1.4.3. デフォルトのBean

このような Bean を作成しつつ、アプリケーションコードにカスタム実装で Bean の一部を簡単にオーバーライドする機能を与えるという非常に便利なパターンは、Quarkus が提供している @DefaultBean を使用することです。これは例を挙げて説明するのが一番です。

ここでは、Quarkusエクステンションが Tracer Bean を提供する必要があると仮定して、アプリケーションコードがそれ自身の 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 を使用しない Library/Quarkus エクステンションによって定義された Bean をオーバーライドする方法

@DefaultBean が推奨されていますが、CDI @Alternative としてBeanをマークし、 @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を有効にします。優先度はデフォルトの Bean を上書きするために任意の数値を指定できますが、複数の代替 Bean がある場合は、最も高い優先度のものが優先されます。
CDI の代替 Beanは、インジェクションと型安全解決の間のみ考慮されます。例えば、デフォルトの実装では、オブザーバー通知を受け取ることになります。

1.4.5. 合成Bean

合成 Beanを登録できると非常に便利なことがあります。合成 Beanのビーン属性は,javaクラス,メソッド,フィールドから派生したものではありません。その代わりに、属性はエクステンションによって指定されます。

CDIコンテナは合成Beanのインスタンス化を制御しないので、依存性注入や他のサービス(インターセプタなど)はサポートされていません。言い換えれば、合成 Bean のインスタンスに必要なすべてのサービスを提供するのはエクステンション次第ということです。

There are several ways to register a synthetic bean in Quarkus. In this chapter, we will cover a use case that can be used to initialize extension beans in a safe manner (compared to 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 recorder 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 recorder in the bytecode")) (1)
                .done();
}
1 「本物の」コンポーネントは、 TestContext を直接注入することができます。

1.5. エクステンションのタイプ

エクステンションの定型的なタイプは複数存在しますが、いくつか挙げてみましょう。

ベアライブラリの実行

これはあまり洗練されていないエクステンションです。これは、ライブラリがGraalVM上で動作するようにするためのパッチのセットで構成されています。可能であれば、これらのパッチはエクステンションの中ではなく、アップストリームで貢献してください。二番目に良いのは、ネイティブイメージのコンパイル時に適用されるパッチである Substrate VM 置換を書くことです。

実行中のフレームワークを取得する

実行時のフレームワークは通常、設定を読み込み、クラスパスとクラスをスキャンしてメタデータ (アノテーションやゲッターなど) を探し、その上にメタモデルを構築し、サービスローダパターンを介してオプションを見つけ、実行呼び出し (リフレクション) やプロキシインターフェイスなどを準備します。これらの操作はビルド時に行われ、メタモデルは実行時に実行されるクラスを生成するレコーダ- DSL に渡され、フレームワークを起動します。

CDI ポータブルエクステンションを動作させる

CDIポータブルエクステンションモデルは非常に柔軟性が高いです。Quarkusが推進するビルドタイムブートの恩恵を受けるには、あまりにも柔軟性が高すぎます。私たちが見てきたほとんどのエクステンションは、このような極端な柔軟性の機能を利用していません。CDIエクステンションをQuarkusに移植する方法は、ビルド時(エクステンションの言い方ではデプロイ時)に様々なBeanを定義するQuarkusエクステンションとして書き換えることです。

2. 技術的な側面

2.1. ブートストラップの 3 つのフェーズと Quarkus 哲学

Quarkusアプリには、3つの異なるブートストラップフェーズがあります。

拡張

This is the first phase, and is done by the ビルドステッププロセッサー. These processors have access to Jandex annotation information and can parse any descriptors and read annotations, but should not attempt to load any application classes. The output of these build steps is some recorded bytecode, using an extension of the ObjectWeb ASM project called Gizmo(ext/gizmo), that is used to actually bootstrap the application at runtime. Depending on the io.quarkus.deployment.annotations.ExecutionTime value of the @io.quarkus.deployment.annotations.Record annotation associated with the build step, the step may be run in a different JVM based on the following two modes.

スタティック初期化

@Record(STATIC_INIT) でバイトコードが記録されている場合は、mainクラスのスタティック初期化メソッドから実行されます。ネイティブビルドの場合、このコードはネイティブビルドプロセスの一部として通常のJVMで実行され、この段階で生成されたリテインドオブジェクトは、イメージマップされたファイルを介してネイティブ実行可能ファイルに直接シリアル化されます。つまり、この段階でフレームワークが起動できれば、そのフレームワークの起動状態がイメージに直接書き込まれるため、イメージの起動時にブートコードを実行する必要がありません。

この段階では、サブストレートVMがネイティブ実行可能ファイルに含まれるいくつかのオブジェクトを許可しないため、実行できる内容にいくつかの制限があります。例えば、この段階でポートのリッスンやスレッドの開始を試みてはいけません。また、スタティック初期化時に実行時設定を読み取ることも禁止されています。

非ネイティブのピュアJVMモードでは、スタティック起動とランタイム起動に実質的な違いはありませんが、スタティック起動が常に最初に実行されることが異なります。このモードでは、記述子の解析とアノテーションのスキャンがビルド時に行われ、関連するクラスやフレームワークの依存関係がビルド出力jarから削除されるため、ネイティブモードと同様のビルドフェーズのエクステンションが利用できます。WildFlyのようなサーバーでは、XMLパーサーなどの展開関連クラスがアプリケーションの存続期間中、貴重なメモリを使用してぶら下がっています。Quarkusは、このようなことをなくし、実行時にロードされるクラスのみが実行時に実際に使用されるようにすることを目指しています。

例として、QuarkusアプリケーションがXMLパーサーをロードする理由は、ユーザーがアプリケーションでXMLを使用している場合のみです。どの設定のXMLパースも、拡張フェーズで行う必要があります。

ランタイム初期化

@Record(RUNTIME_INIT) でバイトコードが記録されている場合は、アプリケーションのメインメソッドから実行されます。このコードはネイティブ実行可能ファイルブートで実行されます。一般的に、このフェーズではできるだけ少ないコードを実行すべきであり、ポートを開く必要があるコードなどに限定すべきです。

@Record(STATIC_INIT) フェーズにできるだけ多くのものを押し込むことで、2つの異なる最適化が可能になります。

  1. ネイティブ実行可能ファイルとピュアJVMの両方のモードで、これによりビルド時に処理が行われたため、アプリを可能な限り高速に起動することができます。また、アプリケーションに必要なクラスやネイティブコードを最小限に抑え、純粋な実行時関連の動作を実現します。

  2. ネイティブ実行可能ファイルモードのもう一つの利点は、サブストレートが使われない機能をより簡単に排除できることです。機能がバイトコードで直接初期化される場合、Substrateはメソッドが一度も呼ばれていないことを検知し、そのメソッドを削除することができます。また、実行時に設定を読み込む場合、サブストレートは設定の内容を推論することができないため、必要な場合に備えてすべての機能を残しておく必要があります。

2.2. プロジェクトのセットアップ

エクステンションプロジェクトは、2つのサブモジュールを持つマルチモジュールプロジェクトとして設定する必要があります。

  1. ビルド時の処理やバイトコードの記録を行うデプロイメント時サブモジュール

  2. ネイティブ実行可能ファイルまたはランタイムJVMでエクステンション動作を提供する実行時動作を含むランタイムサブモジュール

Your runtime artifact should depend on io.quarkus:quarkus-core, and possibly the runtime artifacts of other Quarkus modules if you want to use functionality provided by them.

Your deployment time module should depend on io.quarkus:quarkus-core-deployment, your runtime artifact, and the deployment artifacts of any other Quarkus extensions your own extension depends on. This is essential, otherwise any transitively pulled in extensions will not provide their full functionality.

The Maven and Gradle plugins will validate this for you and alert you to any deployment artifacts you might have forgotten to add.

いかなる場合でも、実行時モジュールはデプロイメントアーティファクトに依存することはできません。これは、すべてのデプロイメント時のコードが実行時スコープに取り込まれることに繋がり、分割の目的を台無しにします。

2.2.1. Maven の使用

io.quarkus:quarkus-extension-maven-plugin をインクルードして、 maven-compiler-pluginquarkus-extension-processor アノテーションプロセッサを検出し、拡張アーティファクトに必要な Quarkus エクステンションメタデータを収集・生成する必要がありますが、もし Quarkus 親 pom を使用していれば自動的に正しい構成を継承します。

You may want to use the create-extension mojo of io.quarkus.platform:quarkus-maven-plugin to create these Maven modules - see the next section.
規約として、デプロイメント時アーティファクトには -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' エクステンションのレイアウトとデフォルトを使用します。

  • それ以外の場合は 'Standalone' エクステンションのレイアウトとデフォルトを使用します。

  • 将来的には他のレイアウトタイプを導入する可能性があります。

You may not specify any parameter to use the interactive mode: mvn io.quarkus.platform:quarkus-maven-plugin:3.9.3:create-extension -N

例として、 my-ext という新しいエクステンションをQuarkusのソースツリーに追加してみましょう:

git clone https://github.com/quarkusio/quarkus.git
cd quarkus
mvn io.quarkus.platform:quarkus-maven-plugin:3.9.3: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 クラスはこのモジュールで生成されます。

    • quarkus-my-ext-integration-test in the integration-tests/my-ext/deployment directory; an empty Jakarta REST Resource class and two test classes (for JVM mode and native mode) are generated in this module.

  • 必要に応じて、これらの3つのモジュールをリンクします。

    • quarkus-my-ext-parentquarkus-extensions-parent<modules> に追加されました。

    • quarkus-my-ext が Quarkus BOM (Bill of Materials) の <dependencyManagement> に追加されました。

    • quarkus-my-ext-deployment が Quarkus BOM (Bill of Materials) の <dependencyManagement> に追加されました。

    • quarkus-my-ext-integration-testquarkus-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 エクステンションのメンテナーが評価する、stablepreviewexperimental のいずれかの成熟度状態。
mojoの name パラメータはオプションです。コマンドラインで指定しなかった場合、プラグインは extensionId からダッシュをスペースに置き換え、各トークンを大文字にすることで導出します。そのため、場合によっては明示的な name を省略することも考えられます。

Mojo で使用できるすべてのオプションについては、 CreateExtensionMojo JavaDoc を参照してください。

2.2.2. Gradle の使用

エクステンションプロジェクトの runtime モジュールに io.quarkus.extension プラグインを適用する必要があります。 このプラグインには META-INF/quarkus-extension.propertiesMETA-INF/quarkus-extension.yml ファイルを生成する extensionDescriptor というタスクが含まれています。 また、このプラグインは io.quarkus:quarkus-extension-processor アノテーションプロセッサを deploymentruntime の両方のモジュールで有効にして、残りの Quarkus extension metadata を収集し生成するようにします。 デプロイメントモジュールの名前は、プラグイン内で deploymentModule プロパティを設定することによって設定することができます。このプロパティは、デフォルトで deployment に設定されています。

plugins {
    id 'java'
    id 'io.quarkus.extension'
}

quarkusExtension {
    deploymentModule = 'deployment'
}

dependencies {
    implementation platform('io.quarkus:quarkus-bom:3.9.3')
}

2.3. ビルドステッププロセッサー

作業は、 ビルドアイテム を生成・消費する ビルドステップ によって拡張時に行われます。プロジェクトビルドのエクステンションに対応するデプロイメント・モジュール内のビルドステップは、自動的に接続され、最終的なビルドアーティファクトを生成するために実行されます。

2.3.1. ビルドステップ

ビルドステップ は、 @io.quarkus.deployment.annotations.BuildStep アノテーションが付与された非静的なメソッドです。各ビルドステップは、前のステージで生成されたアイテムを 消費したり 、後のステージで消費できるアイテムを 生成したりします。ビルドステップは通常、最終的に他のステップで消費されるビルドアイテムを生成するときにのみ実行されます。

ビルドステップは通常、エクステンションのデプロイメントモジュール内のプレーンなクラスに配置されます。このクラスは、拡張プロセス中に自動的にインスタンス化され、 インジェクションを利用します。

2.3.2. ビルドアイテム

ビルドアイテムは、abstractな io.quarkus.builder.item.BuildItem クラスのfinalな具象サブクラスです。それぞれのビルドアイテムは、あるステージから別のステージに渡す必要のある情報の単位を表します。ベースとなる BuildItem クラスは、それ自体を直接サブクラス化することはできません。むしろ、作成 可能な ビルドアイテムのサブクラスの種類( シンプルマルチ)ごとに抽象サブクラスがあります。

ビルドアイテムは、異なるエクステンションが相互に通信するための手段と考えてください。例えば、ビルドアイテムは以下のことができます:

  • データベース設定が存在することを明らかにする

  • データベースの設定を利用する(例:コネクションプールエクステンション、ORMエクステンション)

  • エクステンションに別のエクステンションの動作を要求する。たとえば、新しい CDI Bean を定義するエクステンションの代わりに、ArC エクステンションにその定義を要求します。

これは非常に柔軟なメカニズムです。

BuildItem インスタンスはイミュータブルでなければなりません。プロデューサー/コンシューマーモデルでは、ミューテーションを正しく順序付けることができないからです。これは強制されるものではありませんが、このルールに従わないと競合状態になる可能性があります。
ビルドステップは、他のビルドステップが(推移的に)必要とするビルドアイテムを生成する場合にのみ、実行されます。ビルドステップがビルドアイテムを生成することを確認してください。そうでない場合は、ビルド検証用に ValidationErrorBuildItem を生成するか、生成されたアーティファクト用に ArtifactResultBuildItem を生成すべきです。
2.3.2.1. シンプルなビルドアイテム

シンプルビルドアイテムは io.quarkus.builder.item.SimpleBuildItem を拡張したfinalクラスです。シンプルビルドアイテムは、特定のビルドにおいて、1つのステップでのみ作成できます。ビルド内の複数のステップが同じシンプルビルドアイテムを生成すると宣言した場合、エラーが発生します。シンプルビルドアイテムを利用するビルドステップは、常にそのアイテムを生成したビルドステップの 後に 実行されます。

単一のビルドアイテムの例
/**
 * 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. マルチビルドアイテム

マルチビルドアイテムは、 io.quarkus.builder.item.MultiBuildItem を拡張したfinalクラスです。あるクラスのマルチビルドアイテムは、任意の数のステップで、任意の数生成できますが、マルチビルドアイテムを利用するステップは、それらを生成できるすべてのステップが実行された後のみ実行されます。

マルチビルドアイテムの例
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. 空のビルドアイテム

空のビルドアイテムは、 io.quarkus.builder.item.EmptyBuildItem を拡張するfinalの(通常は空の)クラスです。空のビルドアイテムは、実際には何のデータも持たないビルドアイテムを表しており、空のクラスをインスタンス化することなく、そのようなアイテムを生成したり消費したりすることができます。空のクラスは、それ自体をインスタンス化することはできません。

それらはインスタンス化できないため、いかなる方法でも注入することも、ビルドステップ (または BuildProducer を介して) によって返すこともできません。 空のビルド アイテムを生成するには、ビルド ステップに @Produce(MyEmptyBuildItem.class) でアノテーションを付け、それを @Consume(MyEmptyBuildItem.class) で消費する必要があります。
空のビルドアイテムの例
public final class NativeImageBuildItem extends EmptyBuildItem {
    // empty
}

空のビルドアイテムは、ステップ間の順序付けを可能にする「バリア」を表すことができます。また、一般的なビルドシステムが「疑似ターゲット」を使用するのと同じように、空のビルドアイテムを使用することもできます。

空のビルドアイテムを「疑似ターゲット」スタイルで使用する例
/**
 * 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. バリデーションエラービルドアイテム

これらは、ビルドを失敗させるバリデーションエラーを持つビルドアイテムを表します。これらのビルドアイテムは、CDIコンテナの初期化時に消費されます。

「疑似ターゲット」スタイルでバリデーションエラービルドアイテムを使用する例
@BuildStep
void checkCompatibility(Capabilities capabilities, BuildProducer<ValidationErrorBuildItem> validationErrors) {
    if (capabilities.isMissing(Capability.RESTEASY_REACTIVE)
            && capabilities.isMissing(Capability.RESTEASY_CLASSIC)) {
        validationErrors.produce(new ValidationErrorBuildItem(
                new ConfigurationException("Cannot use both RESTEasy Classic and Reactive extensions at the same time")));
    }
}
2.3.2.5. アーティファクトリゾルトビルドアイテム

これらは、uberjar や thin jar など、ビルドによって生成された実行可能なアーティファクトを含むビルド アイテムを表します。 これらのビルドアイテムを使用して、何も生成せずに常にビルド ステップを実行することもできます。

「疑似ターゲット」スタイルで常に実行されるビルドステップの例
@BuildStep
@Produce(ArtifactResultBuildItem.class)
void runBuildStepThatProducesNothing() {
    // ...
}

2.3.3. インジェクション

ビルドステップを含むクラスは、以下のタイプのインジェクションに対応しています。

  • コンストラクタパラメータ・インジェクション

  • フィールド・インジェクション

  • メソッドパラメータ・インジェクション(ビルドステップ・メソッドの場合のみ)

ビルド・ステップ・クラスは、ビルド・ステップを呼び出すたびにインスタンス化され、注入され、その後は破棄されます。ビルドステップ間では、たとえ同じクラスであっても、ビルドアイテムを介してのみ状態が伝達されるべきです。

finalフィールドはインジェクションの対象にはなりませんが、必要に応じてコンストラクタのパラメータインジェクションで入力することができます。staticフィールドは、インジェクションの対象にはなりません。

注入可能な値の種類は以下の通りです:

ビルドステップのメソッドやそのクラスに注入されたオブジェクトは、そのメソッドの実行時以外に使用しては いけません
インジェクションは、アノテーション・プロセッサを介してコンパイル時に解決され、生成されたコードは、プライベート・フィールドを注入したり、プライベート・メソッドを呼び出したりする権限を持ちません。

2.3.4. 値の生成

ビルドステップは、いくつかの可能な方法で後続のステップのために値を生成することができます。

単純な構築項目が構築ステップで宣言される場合、それはその構築ステップの間に生成されなければならず、さもなければエラーが発生します。ステップに注入されるビルドプロデューサーは、そのステップの外側で使用されては なりません

@BuildStep メソッドは、別のコンシューマーまたは最終出力が必要とするものを生成する場合にのみ呼び出されることに注意してください。特定のアイテムのコンシューマーがいない場合は生成されません。必要なものは、最終的な生成物により異なります。たとえば開発者モードで実行している場合、最終出力では ReflectiveClassBuildItem などの GraalVM 固有のビルドアイテムが要求されないため、これらのアイテムのみを生成するメソッドは呼び出されません。

2.3.5. 値の利用

ビルドステップは、以下の方法で前のステップの値を利用することができます:

通常、他のステップで生成されない単純なビルドアイテムを消費するために含まれるステップはエラーとなる。このように、ステップの実行時に、宣言された値がすべて存在し、かつ非 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 の追加

スキャンされたクラスのインデックスには、外部クラスの依存関係が自動的に含まれることはありません。依存関係を追加するには、groupIdartifactId に対して 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. ビルドステップの依存関係の可視化

It can occasionally be useful to see a visual representation of the interactions between the various build steps. For such cases, adding -Dquarkus.builder.graph-output=build.dot when building an application will result in the creation of the build.dot file in the project’s root directory. See this for a list of software that can open the file and show the actual visual representation.

2.4. 設定

Quarkusの設定は、 SmallRye Config をベースにしています。 SmallRye Config で提供されるすべての機能は、Quarkusでも利用可能です。

エクステンションは、 SmallRye Configの@ConfigMapping を使用して、Extensionが必要とする設定をマッピングする必要があります。これにより、Quarkusは、各設定フェーズにマッピングのインスタンスを自動的に公開し、設定ドキュメントを生成することができます。

2.4.1. 設定フェーズ

設定マッピングは設定フェーズによって厳密に制約されており、対応するフェーズ以外から設定マッピングにアクセスしようとするとエラーが発生します。設定マッピングは、その中に含まれるキーがいつ設定から読み出され、いつアプリケーションから利用できるようになるかを決定するものです。 io.quarkus.runtime.annotations.ConfigPhase で定義されているフェーズは次のとおりです:

フェーズ名 ビルド時に読取、利用可 実行時に利用可 スタティック初期化時の読取 起動時の再読取(ネイティブ実行可能ファイル) 備考

BUILD_TIME

ビルドに影響を与えるものに適しています。

BUILD_AND_RUN_TIME_FIXED

ビルドに影響し、ランタイムコードで表示される必要があるものに適しています。実行時に設定から読み取られません。

BOOTSTRAP

ランタイム設定を外部システム (Consul など) から取得する必要があるが、そのシステムの詳細は設定可能である必要がある場合 (たとえば Consul の URL) に使用されます。これが機能する高レベルの方法は、標準の Quarkus 設定ソース (プロパティーファイル、システムプロパティーなど) を使用し、最終的なランタイム Config オブジェクトを作成するときに Quarkus によって後で考慮される ConfigSourceProvider オブジェクトを生成することです。

RUN_TIME

ビルド時には使用できません。すべてのモードで開始時に読み取ります。

BUILD_TIME 以外のすべての場合、設定マッピングインターフェースと、そこに含まれるすべての設定グループと型は、エクステンションの実行時アーティファクトに配置されるか、そこから到達可能でなければなりません。 BUILD_TIME フェーズの設定マッピングは、エクステンションの実行時アーティファクトまたはデプロイアメントアーティファクトのいずれかに配置されるか、そこから到達可能であれば大丈夫です。

Bootstrap 設定ステップは、runtime-init 中に、他のランタイムステップの に実行されます。これは、このステップの一部として実行されたコードが、ランタイムの初期化ステップで初期化されるものにアクセスできないことを意味します (実行時の合成 CDI Bean はその一例です)。

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 enable();

        /**
         * 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.enable のようなプロパティー名は、次のセグメントに分割できます。

  • quarkus - Quarkusが主張する名前空間で、@ConfigMapping インタフェースのプレフィックスです。

  • enabled - @ConfigGroup アノテーションが付けられた FileConfig クラスの enable フィールドに対応する名前セグメント。

  • file - このクラスの file フィールドに対応する名前セグメント。

  • enable - FileConfig クラスの enable フィールドに対応する名前セグメント。

1 @ConfigMapping のアノテーションは、インターフェースが設定マッピングであることを示し、この場合、 quarkus.log セグメントに対応するものであることを示しています。
2 @ConfigRoot アノテーションは、設定がどの設定フェーズに適用されるかを示したものです。
3 ここで、LoggingProcessor は、@ConfigRoot アノテーションを検出することによって LogConfiguration インスタンスを自動的に挿入します。

上記の例に対応する application.properties は次のようになります。

quarkus.log.file.enable=true
quarkus.log.file.level=DEBUG
quarkus.log.file.path=/tmp/debug.log

これらのプロパティには format が定義されていないため、代わりに @WithDefault のデフォルト値が使用されます。

A configuration mapping name can contain an extra suffix segment for the case where there are configuration mappings for multiple 設定フェーズ. Classes which correspond to the BUILD_TIME and BUILD_AND_RUN_TIME_FIXED may end with BuildTimeConfig or BuildTimeConfiguration, classes which correspond to the RUN_TIME phase may end with RuntimeConfig, RunTimeConfig, RuntimeConfiguration or RunTimeConfiguration while classes which correspond to the BOOTSTRAP configuration may end with BootstrapConfig or BootstrapConfiguration.

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.jboss.org/hibernate/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 タグを付ける必要があります。このタグは、Quarkus生成ツールのマーカーとして使用されるだけでなく、Javadoc生成のための javadoc プロセスでも使用されるなど、2つの目的があります。

より詳細な例:

// @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コメントでインデントを尊重するためには(複数行に広がるリスト項目やインデントされたソースコード)、Eclipseの自動フォーマッターを無効にする必要があります(フォーマッターはビルドに自動的に含まれます)。マーカーは // @formatter:off/// @formatter:on .これらは、個別のコメントと、 // マーカーの後に必須のスペースが必要です。

オープンブロック( -- )は、AsciiDocドキュメントではサポートされていません。他のタイプのブロック(source, admonitions…​)はすべてサポートされています。

デフォルトでは、ドキュメント・ジェネレーターは、ハイフンで区切られたフィールド名を java.util.Map のキーとして使用します。 この動作をオーバーライドするには、 io.quarkus.runtime.annotations.ConfigDocMapKey アノテーションを使用します。

@ConfigMapping(prefix = "quarkus.some")
@ConfigRoot
public interface SomeConfig {
    /**
     * Namespace configuration.
     */
    @WithParentName
    @ConfigDocMapKey("cache-name") (1)
    Map<String, Name> namespace();
}
1 これにより、 quarkus.some."namespace" の代わりに quarkus.some."cache-name" という名前の設定マップキーが生成されます。

It is possible to write a textual explanation for the documentation default value, this is useful when it is generated: @ConfigDocDefault("explain how this is generated").

@ConfigDocEnumValue gives a way to explicitly customize the string displayed in the documentation when listing accepted values for an enum.

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}/config/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]

All documentation should be generated and validated before being committed.

2.5. 条件付きステップを含める

特定の条件下では、特定の @BuildStep のみを含めることができます。@BuildStep アノテーションには、onlyIfonlyIfNot の 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;
    }
}
別のエクステンションの有無を条件としてビルドステップを作成する必要がある場合は、ケイパビリティ を使用できます。

@BuildSteps を使用して、特定のクラスのすべてのビルドステップに一連の条件を適用することもできます。

ビルドステップを @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 メインのビルドステップは、dev モードでのみ実行されます。
3 もう一つのビルドステップは、dev モードでのみ実行されます。

2.6. バイトコード記録

ビルドプロセスの主要なアウトプットの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 を生成するバイトコードが、それを消費するバイトコードよりも先に実行されることがわかっています。

レコーダーに渡すことができるオブジェクトは以下の通りです。

  • プリミティブ

  • 文字列

  • クラス<?> オブジェクト

  • 前回のレコーダー起動時に返されたオブジェクト

  • すべてのプロパティー (またはパブリックフィールド) の引数なしコンストラクターとゲッター/セッターを持つオブジェクト

  • フィールド名と一致するパラメーター名を持つ @RecordableConstructor でアノテーションが付けられたコンストラクターを持つオブジェクト

  • io.quarkus.deployment.recording.RecorderContext#registerSubstitution(Class, Class, Class) メカニズムによる任意のオブジェクト

  • 上記の配列、リスト、マップ

記録するオブジェクトの一部のフィールドを無視する必要がある場合(ビルド時に存在する値を実行時に反映させるべきではない場合)には、そのフィールドに @IgnoreProperty を配置することができます。

クラスがQuarkusに依存できない場合、エクステンションが io.quarkus.deployment.recording.RecordingAnnotationsProvider SPIを実装していれば、Quarkusは任意のカスタムアノテーションを使用することができます。

この同じSPIを使用して、 @RecordableConstructor の代わりになるカスタムアノテーションを提供することも可能です。

2.6.1. レコーダーへの設定の注入

フェーズ RUNTIME または BUILD_AND_RUNTIME_FIXED の設定オブジェクトは、コンストラクターインジェクションを介してレコーダーに注入できます。レコーダが必要とする設定オブジェクトを取得するコンストラクターを作成するだけです。レコーダに複数のコンストラクターがある場合は、Quarkus で使用するコンストラクターに @Inject でアノテーションを付けることができます。レコーダーがランタイム設定を注入したいが、静的初期化時にも使用される場合は、RuntimeValue<ConfigObject> を注入する必要があります。この値は、ランタイムメソッドが呼び出されてた場合にのみ設定されます。

2.6.2. RecorderContext

io.quarkus.deployment.recording.RecorderContext は、バイトコードの記録を強化するためのいくつかの便利なメソッドを提供しています。これには、引数なしのコンストラクタを持たないクラスの作成関数の登録、オブジェクト置換の登録(基本的には、シリアル化不可能なオブジェクトからシリアル化可能なオブジェクトへの変換、およびその逆)、クラスプロキシの作成などがあります。このインターフェースは、 @Record のメソッドにメソッドパラメータとして直接注入することができます。

Calling classProxy with a given fully-qualified class name will create a Class instance that can be passed into a recorder method, and at runtime will be substituted with the class whose name was passed in to classProxy(). However, this method should not be needed in most use cases because directly loading deployment/application classes at processing time in build steps is safe. Therefore, this method is deprecated. Nonetheless, there are some use cases where this method comes in handy, such as referring to classes that were generated in previous build steps using GeneratedClassBuildItem.

2.6.3. Runtime Classpath check

Extensions often need a way to determine whether a given class is part of the application’s runtime classpath. The proper way for an extension to perform this check is to use io.quarkus.bootstrap.classloading.QuarkusClassLoader.isClassPresentAtRuntime.

2.6.4. ステップ実行時間の出力

時には、アプリケーションを実行したときに、各スタートアップタスク(各バイトコードの記録結果)にかかる正確な時間を知りたい場合があります。この情報を確認する最も簡単な方法は、 -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.7. コンテキストと依存性インジェクション

2.7.1. 拡張ポイント

CDIベースのランタイムとして、Quarkusのエクステンションは、多くの場合、エクステンションの動作の一部としてCDI Beanを利用できるようにしています。ただし、Quarkus DIソリューションはCDI Portable Extensionsをサポートしていません。代わりに、Quarkusのエクステンションは、さまざまな Build Time Extension Pointを利用することができます。

2.8. Quarkus Dev UI

エクステンションを Quarkus Dev UIに対応させることで、開発者の利便性を高めることができます。

2.9. エクステンションで定義されたエンドポイント

エクステンションは、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 にあります。

Refer to the Quarkus Vertx HTTP configuration reference for details on how the non-application root path is configured.

2.10. エクステンションヘルスチェック

ヘルスチェックは quarkus-smallrye-health のエクステンションを介して提供されます。これは、livenessとreadinessのチェック機能の両方を提供します。

エクステンションを書くときには、開発者が自分で書かなくても自動的に含まれるようにすることができるエクステンションのヘルスチェックを提供することが有益です。

ヘルスチェックを行うためには、以下のようにしましょう。

  • quarkus-smallrye-health ヘルスチェックを オプションの 依存関係としてランタイムモジュールにインポートすることで、ヘルスチェックが含まれていない場合でもアプリケーションのサイズに影響を与えないようにします。

  • SmallRye Health ガイドに従ってヘルスチェックを作成します。エクステンションの readiness チェックのみを提供することをお勧めします (liveness チェックは、アプリケーションが稼働中で軽量である必要があるというファクトを示すために設計されています)。

  • デプロイメントモジュールに quarkus-smallrye-health-spi ライブラリをインポートします。

  • デプロイメントモジュールに、 HealthBuildItem を生成するビルドステップを追加します。

  • 設定アイテム `quarkus.<extension> を介して、デフォルトでは有効になっているエクステンションのヘルスチェックを無効にする方法を追加します。

以下は、データソースの readiness を検証するための DataSourceHealthCheck を提供する Agroal エクステンションの例です。

@BuildStep
HealthBuildItem addHealthCheck(AgroalBuildTimeConfig agroalBuildTimeConfig) {
    return new HealthBuildItem("io.quarkus.agroal.runtime.health.DataSourceHealthCheck",
            agroalBuildTimeConfig.healthEnabled);
}

2.11. エクステンションメトリクス

quarkus-micrometer エクステンションと quarkus-smallrye-metrics エクステンションは、メトリクスを収集するためのサポートを提供します。互換性についての注意点として、 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 ビルドステップパラメーターを使用して、アクティブなメトリクスエクステンション (smallrye-metrics や micrometer など) に基づいて API 固有メトリクスを宣言または初期化できます。このパターンは、 MetricsFactory::metricsSystemSupported() を使用して、レコーダー内のアクティブなメトリクスエクステンションをテストすることにより、コンシューマーパターンと組み合わせることができます。

メトリクスのサポートはオプションです。エクステンションはビルドステップで Optional<MetricsCapabilityBuildItem> metricsCapability を使用して、有効化されたメトリクスエクステンションの存在をテストできます。追加の設定を使用して、メトリクスの動作を制御することを検討してください。たとえば、データソースメトリクスは高額になる可能性があるため、追加の設定フラグを使用して、個々のデータソースでメトリクス収集を有効にします。

エクステンションのメトリクスを追加するとき、以下のいずれかの状況に陥ることがあります:

  1. エクステンションで使用される基盤となるライブラリーは、特定のメトリクス API (MP Metrics、Micrometer、またはその他) を直接使用します。

  2. 基盤となるライブラリーは、メトリクスを収集するために独自のメカニズムを使用し、Hibernate の Statistics クラスや Vert.x MetricsOptions などの独自の API を使用してランタイムにそれらを利用できるようにします。

  3. 基礎となるライブラリーがメトリクスを提供しない (またはライブラリーがまったくない) ため、インストルメンテーションを追加する必要があります。

2.11.1. ケース1:ライブラリがメトリクス・ライブラリを直接利用する場合

ライブラリーがメトリクス API を直接使用する場合、2 つのオプションがあります。

  • Optional<MetricsCapabilityBuildItem> metricsCapability パラメーターを使用して、ビルドステップでサポートされているメトリクス API (smallrye-metrics や micrometer など) をテストし、それを使用して API 固有の Bean またはビルドアイテムを選択的に宣言または初期化します。

  • MetricsFactory を使用する別のビルドステップを作成し、バイトコードレコーダー内の 'MetricsFactory::metricsSystemSupported()' メソッドを使用して、必要なメトリクス API がサポートされている場合に必要なリソースを初期化します (smallrye-metrics や micrometer など)。

アクティブなメトリクスエクステンションがない場合、またはエクステンションがライブラリーに必要な API をサポートしていない場合、エクステンションはフォールバックを提供しなければならない可能性があります。

2.11.2. ケース 2: ライブラリーが独自のメトリクス API を提供している

独自のメトリクス API を提供するライブラリーの例は 2 つあります。

  • エクステンションは、Agroal が io.agroal.api.AgroalDataSourceMetrics で行うように、インストルメント可能なオブジェクトを定義します。

  • エクステンションは、Jaeger が io.jaegertracing.spi.MetricsFactory で行うように、独自のメトリクスの抽象化を提供します。

2.11.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 の場合、これは MetricFactory API を使用して 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 に基づいてゲージまたはカウンターを構築します。タイマーは、CallableRunnable、または Supplier の実装をラップするか、TimeRecorder を使用して時間のチャンクを蓄積することができます。基盤となるメトリクスエクステンションは、定義された関数を観察または測定するための適切なアーティファクトを作成します。

2.11.2.2. メトリクス API 固有の実装の使用

metrics-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.11.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.12. エクステンションからJSON処理をカスタマイズする

エクステンションはしばしば、エクステンションが提供する型のシリアライザやデシリアライザを登録する必要があります。

このため、Jackson 拡張モジュールと JSON-B 拡張モジュールの両方で、エクステンション内からシリアライザ/デシリアライザを登録する方法を提供しています。

すべての人がJSONを必要とするわけではないことを覚えておいてください。そのため、オプションとする必要があります。

エクステンションがJSON関連のカスタマイズを提供しようとする場合は、JacksonとJSON-Bの両方のカスタマイズを提供することを強くお勧めします。

2.12.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 内で自動的にモジュールを登録します。

If you need more customization capabilities than registering a module, you can produce a CDI bean that implements io.quarkus.jackson.ObjectMapperCustomizer via an AdditionalBeanBuildItem. More info about customizing Jackson can be found on the JSON guide Configuring JSON support

2.12.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 エクステンションは、生成されたビルドアイテムを使用して、シリアライザー/デシリアライザーを自動的に登録します。

If you need more customization capabilities than registering a serializer or a deserializer, you can produce a CDI bean that implements io.quarkus.jsonb.JsonbConfigCustomizer via an AdditionalBeanBuildItem. More info about customizing JSON-B can be found on the JSON guide Configuring JSON support

2.13. 開発モードとの連携

開発モードとの統合や、現在の状態に関する情報を得るために使用できる様々なAPIがあります。

2.13.1. 再起動のハンドリング

Quarkusの起動時には、特にこの起動に関する情報を与える io.quarkus.deployment.builditem.LiveReloadBuildItem が存在することが保証されています。特に、

  • クリーンスタートなのか、ライブリロードなのか

  • ライブリロードで、変更されたファイルやクラスがリロードのきっかけになっているかどうか

また、静的なフィールドに頼ることなく、再起動の間に情報を保存するために使用できるグローバルなコンテキストマップを提供しています。

2.13.2. ライブリロードのトリガー

しかし、すべてのアプリケーションがHTTPアプリケーションであるとは限らず、エクステンションによっては他のイベントに基づいてライブリロードをトリガしたい場合もあります。これを実現するには、実行時モジュールで io.quarkus.dev.spi.HotReplacementSetup を実装し、その実装をリストアップする META-INF/services/io.quarkus.dev.spi.HotReplacementSetup を追加する必要があります。

起動時には setupHotDeployment メソッドが呼び出され、提供された io.quarkus.dev.spi.HotReplacementContext を使って変更されたファイルのスキャンを開始することができます。

2.14. エクステンションのテスト

Quarkus エクステンションのテストは io.quarkus.test.QuarkusUnitTest JUnit 5 拡張モジュールを使用してください。このエクステンションを使用すると、特定の機能をテストする Arquillian スタイルのテストを行うことができます。ユーザーアプリケーションのテストは io.quarkus.test.junit.QuarkusTest を経由して行う必要があるため、ユーザーアプリケーションのテストを目的としたものではありません。主な違いは、 QuarkusTest は実行開始時にアプリケーションを起動するだけで、 QuarkusUnitTest は各テストクラスごとにカスタムの Quarkus アプリケーションを展開するという点です。

これらのテストもデプロイメントモジュールに配置される必要があります。もし追加のQuarkusモジュールがテストに必要な場合は、それらのデプロイメントモジュールもテストスコープ付きの依存関係として追加しなければなりません。

なお、 QuarkusUnitTest は、 quarkus-junit5-internal モジュールに入っています。

テストクラスの例は次のようになります。

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.QuarkusUnitTest;
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 QuarkusUnitTest config = new QuarkusUnitTest()
            .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 QuarkusUnitTest エクステンションは、静的フィールドと一緒に使用する必要があります。静的でないフィールドで使用した場合、テストアプリケーションは開始されません。
2 このプロデューサーは、テストするアプリケーションを構築するために使用されます。Shrinkwrap を使用して、テストする JavaArchive を作成します。
3 テストデプロイメントからテストケースに直接 Bean を注入することが可能です。
4 このメソッドは、ヘルスチェックサーブレットを直接呼び出し、応答を検証します。
5 このメソッドは、注入されたヘルスチェック Bean を使用して、期待される結果を返していることを確認します。

エクステンションがビルド時に正しく失敗することをテストする場合は、setExpectedException メソッドを使用します。

package io.quarkus.hibernate.orm;

import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusUnitTest;
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 QuarkusUnitTest runner = new QuarkusUnitTest()
            .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 に伝えます。

2.15. ホットリロードのテスト

開発モードでエクステンションが正しく動作し、アップデートを正しく処理できるかどうかを検証するテストを書くことも可能です。

ほとんどのエクステンションでは、これは「箱から出してすぐに」動作しますが、この機能が期待通りに動作しているかどうかを確認するためにスモークテストを行うことをお勧めします。このテストには 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.16. ネイティブ実行可能ファイルのサポート

そこで 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を必要とし、ネイティブイメージのビルド中に有効にする必要があることをQuarkusに伝える便利な方法です。この機能を使用する場合は、 ネイティブおよびSSLガイド の、自動的にSSLサポートを提供するエクステンションのリストに、作成したエクステンションを追加することを忘れないでください。

2.17. IDE サポートのヒント

2.17.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.18. トラブルシューティング / デバッグのヒント

2.18.1. 生成・変換されたクラスの検査

Quarkusでは、ビルドフェーズで多くのクラスが生成され、多くの場合、既存のクラスも変換されます。エクステンションの開発中に、生成されたバイトコードや変換されたクラスを見ることができるのは、非常に便利なことです。

If you set the quarkus.package.decompiler.enabled property to true then Quarkus will download and invoke the Vineflower decompiler and dump the result in the decompiled directory of the build tool output (target/decompiled for Maven for example).

このプロパティは、通常のプロダクションビルド時にのみ機能します(つまり、devモード/テストでは機能しません)。また、 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 に対応します。

ディレクトリーに書き込まれた各クラスのログに行が表示されます。

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.18.2. マルチモジュールのMavenプロジェクトと開発モード

example "モジュールを含むマルチモジュールのMavenプロジェクトでエクステンションを開発することはよくあります。しかし、開発モードでサンプルを実行する場合は、ローカルプロジェクトの依存関係を除外するために、 -DnoDeps システムプロパティを使用する必要があります。そうしないと、Quarkusがエクステンションクラスを監視しようとするため、クラスの読み込みに問題が生じる可能性があります。

./mvnw compile quarkus:dev -DnoDeps

2.18.3. インデクサーには外部依存関係は含まれていません

@BuildStepIndexDependencyBuildItem アーティファクトを追加することを忘れないでください。

2.19. サンプルテストエクステンション

エクステンションの処理のリグレッションをテストするために使われるエクステンションがあります。これは https://github.com/quarkusio/quarkus/tree/main/integration-tests/test-extension/extension ディレクトリにあります。このセクションでは、test-extension のコードを使って、エクステンションの作者が通常行う必要のあるタスクについて触れます。

2.19.1. フィーチャーとケイパビリティ

2.19.1.1. 特徴

フィーチャー とは、エクステンションが提供する機能のことです。フィーチャーの名前は、アプリケーションの起動時にログに表示されます。

起動時の行の例
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 ランタイムイメージにインストールされているフィーチャーのリスト

A feature can be registered in a ビルドステッププロセッサー method that produces a FeatureBuildItem:

TestProcessor#feature()
    @BuildStep
    FeatureBuildItem feature() {
        return new FeatureBuildItem("test-extension");
    }

フィーチャー名には小文字のみを使用し、単語はダッシュで区切ります。例: security-jpa 。1 つのエクステンションが提供するフィーチャーは最大でも 1 つで、その名前は一意でなければなりません。複数のエクステンションが同じ名前のフィーチャーを登録した場合、ビルドは失敗します。

The feature name should also map to a label in the extension’s devtools/common/src/main/filtered/extensions.json entry so that the feature name displayed by the startup line matches a label that one can use to select the extension when creating a project using the Quarkus maven plugin as shown in this example taken from the Writing JSON REST Services guide where the rest-jackson feature is referenced:

mvn io.quarkus.platform:quarkus-maven-plugin:3.9.3:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json \
    -DclassName="org.acme.rest.json.FruitResource" \
    -Dpath="/fruits" \
    -Dextensions="rest,rest-jackson"
cd rest-json
2.19.1.2. ケイパビリティ

ケイパビリティ は、他のエクステンションから問い合わせ可能な技術的能力を表します。1つのエクステンションが複数のケイパビリティを提供することも、複数のエクステンションが同じケイパビリティを提供することもできます。デフォルトでは、ケイパビリティはユーザーに表示されません。エクステンションの存在を確認する際には、クラスパスベースのチェックではなく、ケイパビリティを使用する必要があります。

Capabilities can be registered in a ビルドステッププロセッサー method that produces a CapabilityBuildItem:

TestProcessor#capability()
    @BuildStep
    void capabilities(BuildProducer<CapabilityBuildItem> capabilityProducer) {
        capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-transactions"));
        capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-metrics"));
    }

エクステンションは、 Capabilities ビルドアイテムを使用して、登録されたケイパビリティを消費することができます。

TestProcessor#doSomeCoolStuff()
    @BuildStep
    void doSomeCoolStuff(Capabilities capabilities) {
        if (capabilities.isPresent(Capability.TRANSACTIONS)) {
          // do something only if JTA transactions are in...
        }
    }

ケイパビリティは、io.quarkus.security.jpa のように、Java パッケージの命名規則に従う必要があります。コアエクステンションによって提供されるケイパビリティは、io.quarkus.deployment.Capability 列挙型にリストされている必要があり、それらの名前は常に io.quarkus の接頭辞で始まる必要があります。

2.19.2. アノテーションを定義する Bean

CDI レイヤーは、明示的に登録されているか、 2.5.1. Bean 定義アノテーション で定義されている Bean 定義アノテーションに基づいて検出する CDI Bean を処理します。このアノテーションセットを拡張し、TestProcessor#registerBeanDefinningAnnotations の例が示すように BeanDefiningAnnotationBuildItem を使用してエクステンションプロセスにアノテーションを含めることができます。

アノテーションを定義する Bean の登録
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.19.3. コンフィグをオブジェクトにパースする

エクステンションの主な目的の1つは、動作の設定段階を実行段階から完全に分離することです。フレームワークは起動時に設定の解析や読み込みを行うことが多いですが、これをビルド時に行うことで、xmlパーサーなどのフレームワークへの実行時の依存を減らし、解析にかかる起動時間を短縮することができます。

An example of parsing an XML config file using JAXB is shown in the TestProcessor#parseServiceXmlConfig method:

Parsing an XML Configuration into Runtime XmlConfig Instance
    @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 見つかった場合は、JAXB コンテキストを使用して XmlConfig.class を解析します。

ビルド環境で使用可能な /config.xml リソースがない場合、null の RuntimeServiceBuildItem が返され、生成されている RuntimeServiceBuildItem に基づく後続のロジックは実行されません。

Typically, one is loading a configuration to create some runtime component/service as parseServiceXmlConfig is doing. We will come back to the rest of the behavior in parseServiceXmlConfig in the following 非CDIサービスの管理 section.

何らかの理由で設定を解析し、エクステンションプロセッサーの他のビルドステップで使用する必要がある場合は、解析された XmlConfig インスタンスを渡すために XmlConfigBuildItem を作成する必要があります。

If you look at the XmlConfig code you will see that it does carry around the JAXB annotations. If you don’t want these in the runtime image, you could clone the XmlConfig instance into some POJO object graph and then replace XmlConfig with the POJO class. We will do this in ネイティブイメージにおいてクラスを置換する.

2.19.4. Jandex を使用したデプロイメントのスキャン

エクステンションが、処理が必要なBeanをマークするアノテーションやインターフェースを定義している場合、Javaアノテーション・インデクサーとオフライン・リフレクション・ライブラリであるJandex APIを使って、これらのBeanを見つけることができます。以下の TestProcessor#scanForBeans メソッドは、 IConfigConsumer インターフェースも実装している @TestAnnotation でアノテーションされたBeanを見つける方法を示しています。

Jandex の使用例
    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 クラスを TestBeanBuildItem に保存して、Bean インスタンスとインタラクトする後の RUNTIME_INIT ビルドステップで使用できるようにします。

2.19.5. エクステンション Bean とのインタラクション

io.quarkus.arc.runtime.BeanContainer インターフェイスを使用して、エクステンション Bean とインタラクトできます。次の configureBeans メソッドは、前のセクションでスキャンされた Bean とのインタラクションを示しています。

CDI BeanContainer インターフェイスの使用
// 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.19.6. 非CDIサービスの管理

A common purpose for an extension is to integrate a non-CDI aware service into the CDI based Quarkus runtime. Step 1 of this task is to load any configuration needed in a STATIC_INIT build step as we did in コンフィグをオブジェクトにパースする. Now we need to create an instance of the service using the configuration. Let’s return to the TestProcessor#parseServiceXmlConfig method to see how this can be done.

非CDIサービスの作成
// 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.19.6.1. サービスの開始

ビルドフェーズでのサービスの作成を記録したので、起動時にランタイムでサービスを開始する方法を記録する必要があります。これは、TestProcessor#startRuntimeService メソッドに示されている RUNTIME_INIT ビルドステップを使用して行います。

非CDIサービスの開始/停止
// 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 Produce a ServiceStartBuildItem to indicate the startup of a service. See スタートアップとシャットダウンのイベント for details.
5 ランタイムレコーダはサービスインスタンス参照を取得し、その startService メソッドを呼び出します。
6 ランタイムレコーダーは、サービスインスタンスの stopService メソッドの呼び出しを Quarkus の ShutdownContext に登録します。

RuntimeXmlConfigService のコードは、 RuntimeXmlConfigService.java で確認できます。

RuntimeXmlConfigService が開始されたことを検証するテストケースは、ConfiguredBeanTestNativeImageITtestRuntimeXmlConfigService テストにあります。

2.19.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 で通知されるのを確認します。

What is the relevance of startup and shutdown events for extension authors? We have already seen the use of a ShutdownContext to register a callback to perform shutdown tasks in the サービスの開始 section. These shutdown tasks would be called after a ShutdownEvent had been sent.

StartupEvent は、すべての io.quarkus.deployment.builditem.ServiceStartBuildItem プロデューサーが消費された後に実行されます。これは、StartupEvent が確認されるとアプリケーションコンポーネントが起動することを想定したサービスがエクステンションにある場合に、そのようなサービスを起動するためのランタイムコードを呼び出すビルドステップは、StartupEvent の送信前にランタイムコードが実行されるように ServiceStartBuildItem を作成する必要があることを意味します。ServiceStartBuildItem の作成については前のセクションで説明しましたが、確認のためにここでも繰り返します。

ServiceStartBuildItem の生成例
// TestProcessor#startRuntimeService
    @BuildStep
    @Record(RUNTIME_INIT)
    ServiceStartBuildItem startRuntimeService(TestRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem,
            RuntimeServiceBuildItem serviceBuildItem) throws IOException {
...
        return new ServiceStartBuildItem("RuntimeXmlConfigService"); (1)
    }
1 ServiceStartBuildItem を生成し、これが StartupEvent の送信前に実行する必要のあるサービス開始ステップであることを示します。

2.19.8. ネイティブイメージで使用するリソースの登録

ビルド時にすべての設定またはリソースを消費できるわけではありません。ランタイムがアクセスする必要のあるクラスパスリソースがある場合は、これらのリソースをネイティブイメージにコピーする必要があることをビルドフェーズに通知する必要があります。そのためには、リソースバンドルの場合、1 つ以上の NativeImageResourceBuildItem または NativeImageResourceBundleBuildItem を生成することによって行われます。その例を、この registerNativeImageResources ビルドステップのサンプルに示します。

リソースと ResourceBundle の登録
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.19.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 で登録するコードを生成する方法と比べると効率的ではありません。

これは、リフレクション用のクラスを登録する代わりに、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.19.10. オブジェクトの置換

ランタイムに渡されるビルドフェーズ中に作成されたオブジェクトは、ビルド時の状態からランタイムの起動時に作成および設定できるよう、デフォルトのコンストラクターを持っている必要があります。オブジェクトにデフォルトのコンストラクターがない場合、拡張アーティファクトの生成中に次のようなエラーが表示されます。

DSAPublicKey シリアル化エラー
	[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 の実装例を次に示します。

DSAPublicKeyObjectSubstitution 例
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 シリアル化メソッドは、デフォルトのコンストラクターなしでオブジェクトを取得し、DSAPublicKey の再作成に必要な情報を含む KeyProxy を作成します。
2 デシリアル化メソッドは 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.19.11. ネイティブイメージにおいてクラスを置換する

Graal SDK は、ネイティブイメージ内のクラスの置換をサポートしています。 XmlConfig/XmlData のクラスを、JAXB アノテーションに依存しないバージョンのクラスに置き換える方法の例を以下に示します。

Substitution of XmlConfig/XmlData クラスの例
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.1. エクステンションの継続的なテスト

エクステンションの作者が、Quarkusの最新のスナップショットに対して自分のエクステンションを毎日簡単にテストできるようにするために、QuarkusはEcosystem CIという概念を導入しました。Ecosystem CIの READMEには、この機能を利用するためにGitHub Actionsジョブをセットアップする方法の詳細が記載されており、この ビデオではそのプロセスの概要を説明しています。

3.2. エクステンションを registry.quarkus.io で公開

エクステンションを Quarkusツールに公開する前に、以下の要件が満たされていることを確認してください。

  • quarkus-extension.yaml ファイル(エクステンションの runtime/ モジュール内)には、最小限のメタデータが設定されている:

    • name

    • description (推奨アプローチのとおり runtime/pom.xml の <description> 要素にすでに設定されていない限り)

  • エクステンションがMaven Centralで公開されていること

  • Your extension repository is configured to use the Ecosystem CI.

それから、 Quarkus Extension Catalogextensions/ ディレクトリに your-extension.yaml ファイルを追加するプルリクエストを作成する必要があります。YAMLは以下のような構造になっていなければなりません。

group-id: <YOUR_EXTENSION_RUNTIME_GROUP_ID>
artifact-id: <YOUR_EXTENSION_RUNTIME_ARTIFACT_ID>
When your repository contains multiple extensions, you need to create a separate file for each individual extension, not just one file for the entire repository.

以上で完了です。プルリクエストがマージされると、スケジュールされたジョブがMaven Centralの新しいバージョンをチェックし、 Quarkus エクステンションレジストリをアップデートします。

関連コンテンツ