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コード置換のホストにより、アプリケーションのニーズに基づいたデッドコードの排除を支援します

    • This is application dependent and cannot really be shared in the library itself

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

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

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

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

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

1.3. 設定を公開する方法

Quarkus simplifies the most common usages. This means that its defaults might be different from the library it integrates.

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

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

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

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

エクステンションが追加のコンフィグソースを提供し、それらがスタティック初期化中に必要な場合は、それらを StaticInitConfigSourceProviderBuildItem に登録する必要があります。スタティック初期化での設定は、アプリケーション起動時の二重の初期化を避けるため、追加のソースをスキャンしません。

1.4. CDI経由でコンポーネントを公開

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

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

An extension can produce an AdditionalBeanBuildItem to instruct the container to read a bean definition from a class as if it was part of the original application:

登録されている 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コンテナは スタティック初期化ブートストラップフェーズ の間に起動されています。

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

A very useful pattern of creating such beans but also giving application code the ability to easily override some beans with custom implementations, is to use the @DefaultBean that Quarkus provides. This is best explained with an example.

ここでは、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を使用しないライブラリ/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のBean属性は,javaクラス,メソッド,フィールドから派生したものではありません。その代わりに、属性はエクステンションによって指定されます。

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

Quarkusで 合成Bean を登録する方法はいくつかあります。この章では、( bean_init と比較して)安全な方法でエクステンション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 置換を書くことです。

フレームワークを実行

A framework at runtime typically reads configuration, scan the classpath and classes for metadata (annotations, getters etc.), build a metamodel on top of which it runs, find options via the service loader pattern, prepare invocation calls (reflection), proxy interfaces, etc. + These operations should be done at build time and the metamodel be passed to the recorder DSL that will generate classes that will be executed at runtime and boot the framework.

CDI ポータブルエクステンションの実行

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

2. 技術的な側面

2.1. Bootstrapの3つのフェーズとQuarkusの哲学

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

拡張

これは最初のフェーズであり、 link:#Build Step Processors[Build Step Processors]によって行われます。これらのプロセッサはJandexのアノテーション情報にアクセスすることができ、任意の記述子を解析してアノテーションを読み取ることができますが、任意のアプリケーションクラスのロードを試みるべきではありません。これらのビルドステップの出力は、Gizmo(ext/gizmo)と呼ばれるObjectWeb ASMプロジェクトのエクステンションを使用して、記録されたバイトコードであり、実行時にアプリケーションを実際にブートストラップするために使用されます。ビルド・ステップに関連する @io.quarkus.deployment.annotations.Record アノテーションの io.quarkus.deployment.annotations.ExecutionTime 値に応じて、そのステップは以下の2つのモードに基づいて異なるJVMで実行されることがあります。

スタティック初期化

@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でエクステンション動作を提供する実行時動作を含む実行時サブモジュール

実行時アーティファクトは、 io.quarkus:quarkus-core や 、おそらく他のQuarkusモジュールの実行時アーティファクトに依存する必要があります。デプロイメント時モジュールは、 io.quarkus:quarkus-core-deployment や、実行時アーティファクト、およびおそらく他のQuarkusモジュールが提供する機能を使用したい場合は、そのデプロイメントアーティファクトに依存する必要があります。

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

2.2.1. Mavenの使用

You will need to include the io.quarkus:quarkus-extension-maven-plugin and configure the maven-compiler-plugin to detect the quarkus-extension-processor annotation processor to collect and generate the necessary Quarkus extension metadata for the extension artifacts, if you are using the Quarkus parent pom it will automatically inherit the correct configuration.

io.quarkus:quarkus-maven-plugincreate-extension mojo は、新しい Quarkus エクステンションの実装に必要な Maven モジュールのスタブを生成するために使用できます。次のセクションをご覧ください。
規約として、デプロイメント時アーティファクトには -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を使用して新しいQuarkusCoreエクステンションモジュールを作成

Quarkus は、 create-extension Maven Mojo を提供し、エクステンションプロジェクトを初期化します。

オプションの自動検出が試行されます:

  • quarkus (Quarkus Core) または quarkus/extensions ディレクトリから使用すると、'Quarkus Core' エクステンションのレイアウトとデフォルトが使用されます。

  • -DgroupId=io.quarkiverse.[extensionId] を使用すると、'Quarkiverse' エクステンションのレイアウトとデフォルトを使用します。

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

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

インタラクティブ・モードを使用する場合は、パラメータを指定する必要はありません: mvn io.quarkus.platform:quarkus-maven-plugin:2.11.1.Final:create-extension -N

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

git clone https://github.com/quarkusio/quarkus.git
cd quarkus
mvn io.quarkus.platform:quarkus-maven-plugin:2.11.1.Final:create-extension -N \
    -DextensionId=my-ext \
    -DextensionName="My Extension" \
    -DextensionDescription="Do something useful."
By default, the groupId, version, quarkusVersion, namespaceId, and namespaceName will be consistent with other Quarkus core extensions.
The extension description is important as it is displayed on https://code.quarkus.io/, when listing extensions with the Quarkus CLI, etc.

上記の一連のコマンドは、次のような動作をします。

  • 4つの新しいMavenモジュールを作成します。

    • extensions/my-ext ディレクトリ内に quarkus-my-ext-parent

    • extensions/my-ext/runtime ディレクトリ内に quarkus-my-ext

    • extensions/my-ext/deployment ディレクトリに quarkus-my-ext-deployment ; 基本的な MyExtProcessor クラスはこのモジュールで生成されます。

    • integration-tests/my-ext/deployment ディレクトリに quarkus-my-ext-integration-test ; 空の JAX-RS Resource クラスと 2 つのテストクラス (JVM モードとネイティブモード用) がこのモジュールで生成されます。

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

    • quarkus-my-ext-parent<modules>quarkus-extensions-parent が追加されます

    • QuarkusのBOM(Bill of Materials) bom/application/pom.xml<dependencyManagement>quarkus-my-ext が追加されます。

    • QuarkusのBOM(Bill of Materials) bom/application/pom.xml<dependencyManagement>quarkus-my-ext-deployment が追加されます。

    • quarkus-integration-tests-parent<modules>quarkus-my-ext-integration-test が追加されます

You also have to fill the quarkus-extension.yaml template file that describe your extension inside the runtime module src/main/resources/META-INF folder.

This is the quarkus-extension.yaml template of the quarkus-agroal extension, you can use it as an example:

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 the name of the extension that will be displayed to users
2 keywords that can be used to find the extension in the extension catalog
3 link to the extension’s guide or documentation
4 categories under which the extension should appear on code.quarkus.io, could be omitted, in which case the extension will still be listed but not under any specific category
5 maturity status, which could be stable, preview or experimental, evaluated by extension maintainers
mojoの name パラメータはオプションです。コマンドラインで指定しなかった場合、プラグインは extensionId からダッシュをスペースに置き換え、各トークンを大文字にすることで導出します。そのため、場合によっては明示的な name を省略することも考えられます。

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

2.2.2. Gradleの使用

You will need to apply the io.quarkus.extension plugin in the runtime module of your extension project. The plugin includes the extensionDescriptor task that will generate META-INF/quarkus-extension.properties and META-INF/quarkus-extension.yml files. The plugin also enables the io.quarkus:quarkus-extension-processor annotation processor in both deployment and runtime modules to collect and generate the rest of the Quarkus extension metadata. The name of the deployment module can be configured in the plugin by setting the deploymentModule property. The property is set to deployment by default:

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

quarkusExtension {
    deploymentModule = 'deployment'
}

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

このプラグインはまだ実験的なもので、同等のMavenプラグインが行うようなエクステンションの依存関係の検証は行いません。

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 インスタンスはイミュータブルでなければなりません。プロデューサー/コンシューマーモデルでは、ミューテーションを正しく順序付けることができないからです。これは強制されるものではありませんが、このルールに従わないと競合状態になる可能性があります。
2.3.2.1. シンプルビルドアイテム

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

Example of a single build item
/**
 * 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の(通常は空の)クラスです。空のビルドアイテムは、実際には何のデータも持たないビルドアイテムを表しており、空のクラスをインスタンス化することなく、そのようなアイテムを生成したり消費したりすることができます。空のクラスは、それ自体をインスタンス化することはできません。

As they cannot be instantiated, they cannot be injected by any means, nor be returned by a build step (or via a BuildProducer). To produce an empty build item you must annotate the build step with @Produce(MyEmptyBuildItem.class) and to consume it by @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.3. 注入

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

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

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

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

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

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

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

Objects which are injected into a build step method or its class must not be used outside that method’s execution.
インジェクションは、アノテーション・プロセッサを介してコンパイル時に解決され、生成されたコードは、プライベート・フィールドを注入したり、プライベート・メソッドを呼び出したりする権限を持ちません。

2.3.4. 値の生成

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

If a simple build item is declared on a build step, it must be produced during that build step, otherwise an error will result. Build producers, which are injected into steps, must not be used outside that step.

Note that a @BuildStep method will only be called if it produces something that another consumer or the final output requires. If there is no consumer for a particular item then it will not be produced. What is required will depend on the final target that is being produced. For example, when running in developer mode the final output will not ask for GraalVM-specific build items such as ReflectiveClassBuildItem, so methods that only produce these items will not be invoked.

2.3.5. 値の利用

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

Normally it is an error for a step which is included to consume a simple build item that is not produced by any other step. In this way, it is guaranteed that all the declared values will be present and non-null when a step is run.

Sometimes a value isn’t necessary for the build to complete, but might inform some behavior of the build step if it is present. In this case, the value can be optionally injected.

Multi build values are always considered optional. If not present, an empty list will be injected.
2.3.5.1. 弱い値の生成

Normally a build step is included whenever it produces any build item which is in turn consumed by any other build step. In this way, only the steps necessary to produce the final artifact(s) are included, and steps which pertain to extensions which are not installed or which only produce build items which are not relevant for the given artifact type are excluded.

For cases where this is not desired behavior, the @io.quarkus.deployment.annotations.Weak annotation may be used. This annotation indicates that the build step should not automatically be included solely on the basis of producing the annotated value.

Example of producing a build item weakly
/**
 * 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 This method (not provided in this example) would generate the class using the ASM API.

Certain types of build items are generally always consumed, such as generated classes or resources. An extension might produce a build item along with a generated class to facilitate the usage of that build item. Such a build step would use the @Weak annotation on the generated class build item, while normally producing the other build item. If the other build item is ultimately consumed by something, then the step would run and the class would be generated. If nothing consumes the other build item, the step would not be included in the build process.

In the example above, GeneratedClassBuildItem would only be produced if ExecutorClassBuildItem is consumed by some other build step.

Note that when using bytecode recording, the implicitly generated class can be declared to be weak by using the optional attribute of the @io.quarkus.deployment.annotations.Record annotation.

Example of using a bytecode recorder where the generated class is weakly produced
/**
 * This build step is only run if something consumes the ExecutorBuildItem.
 */
@BuildStep
@Record(value = ExecutionTime.RUNTIME_INIT, optional = true) (1)
ExecutorBuildItem createExecutor( (2)
        ExecutorTemplate executorTemplate,
        ThreadPoolConfig threadPoolConfig
) {

    return new ExecutorBuildItem(
        setupTemplate.setupRunTime(
            shutdownContextBuildItem,
            threadPoolConfig,
            launchModeBuildItem.getLaunchMode()
        )
    );
}
1 Note the optional attribute.
2 This example is using recorder proxies; see the section on bytecode recording for more information.

2.3.6. アプリケーションアーカイブ

@BuildStep アノテーションは、クラスパス上のどのアーカイブが「アプリケーション・アーカイブ」とみなされ、したがってインデックスが作成されるかを決定するマーカーファイルを登録することもできます。これは applicationArchiveMarkers を介して行われます。たとえば、ArCエクステンションでは META-INF/beans.xml を登録していますが、これは beans.xml ファイルがあるクラスパス上のすべてのアーカイブがインデックスされることを意味します。

2.3.7. スレッドコンテキストクラスローダーの使用

The build step will be run with a TCCL that can load user classes from the deployment in a transformer-safe way. This class loader only lasts for the life of the augmentation, and is discarded afterwards. The classes will be loaded again in a different class loader at runtime. This means that loading a class during augmentation does not stop it from being transformed when running in the development/test mode.

2.3.8. Adding external JARs to the indexer with IndexDependencyBuildItem

The index of scanned classes will not automatically include your external class dependencies. To add dependencies, create a @BuildStep that produces IndexDependencyBuildItem objects, for a groupId and artifactId.

It is important to specify all the required artifacts to be added to the indexer. No artifacts are implicitly added transitively.

The Amazon Alexa extension adds dependent libraries from the Alexa SDK that are used in Jackson JSON transformations, in order for the reflective classes to identified and included at 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"));
    }

With the artifacts added to the Jandex indexer, you can now search the index to identify classes implementing an interface, subclasses of a specific class, or classes with a target annotation.

For example, the Jackson extension uses code like below to search for annotations used in JSON deserialization, and add them to the reflective hierarchy for BUILD_TIME analysis.

    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 -Djboss.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. 設定

Configuration in Quarkus is based on SmallRye Config, an implementation of the MicroProfile Config specification. All the standard features of MP-Config are supported; in addition, there are several extensions which are made available by the SmallRye Config project as well as by Quarkus itself.

The value of these properties is configured in a application.properties file that follows the MicroProfile config format.

Configuration of Quarkus extensions is injection-based, using annotations.

2.4.1. 設定キー

Leaf configuration keys are mapped to non-private fields via the @io.quarkus.runtime.annotations.ConfigItem annotation.

Though the SmallRye Config project is used for implementation, the standard @ConfigProperty annotation does not have the same semantics that are needed to support configuration within extensions.

Configuration keys are normally derived from the field names that they are tied to. This is done by de-camel-casing the name and then joining the segments with hyphens (-). Some examples:

  • bindAddress becomes bind-address

  • keepAliveTime becomes keep-alive-time

  • requestDNSTimeout becomes request-dns-timeout

The name can also be explicitly specified by giving a name attribute to the @ConfigItem annotation.

Though it is possible to override the configuration key name using the name attribute of @ConfigItem, normally this should only be done in cases where (for example) the configuration key name is the same as a Java keyword.

2.4.2. 設定値の型

The type of the field with the @ConfigItem annotation determines the conversion that is applied to it. Quarkus extensions may use the full range of configuration types made available by SmallRye Config, which includes:

  • すべてのプリミティブ型とプリミティブラッパー型

  • String

  • Any type which has a constructor accepting a single argument of type String or CharSequence

  • Any type which has a static method named of which accepts a single argument of type String

  • Any type which has a static method named valueOf or parse which accepts a single argument of type CharSequence or String

  • java.time.Duration

  • java.util.regex.Pattern

  • java.nio.file.Path

  • io.quarkus.runtime.configuration.MemorySize データサイズを表す

  • java.net.InetSocketAddress, java.net.InetAddress and org.wildfly.common.net.CidrAddress

  • java.util.Locale 文字列の値がIETF BCP 47の言語タグである場合

  • java.nio.charset.Charset where the string value is a canonical name or an alias

  • java.time.ZoneId where the string value is parsed via java.time.ZoneId.of(String)

  • A List or Optional of any of the above types

  • OptionalInt, OptionalLong, OptionalDouble

In addition, custom converters may be registered by adding their fully qualified class name in file META-INF/services/org.eclipse.microprofile.config.spi.Converter.

Though these implicit converters use reflection, Quarkus will automatically ensure that they are loaded at the appropriate time.

2.4.2.1. オプション値

If the configuration type is one of the optional types, then empty values are allowed for the configuration key; otherwise, specification of an empty value will result in a configuration error which prevents the application from starting. This is especially relevant to configuration properties of inherently emptiable values such as List, Set, and String. Such value types will never be empty; in the event of an empty value, an empty Optional is always used.

2.4.3. 設定デフォルト値

A configuration item can be marked to have a default value. The default value is used when no matching configuration key is specified in the configuration.

Configuration items with a primitive type (such as int or boolean) implicitly use a default value of 0 or false. The sole exception to this rule is the char type which does not have an implicit default value.

A property with a default value is not implicitly optional. If a non-optional configuration item with a default value is explicitly specified to have an empty value, the application will report a configuration error and will not start. If it is desired for a property to have a default value and also be optional, it must have an Optional type as described above.

2.4.4. 設定グループ

Configuration values are always collected into grouping classes which are marked with the @io.quarkus.runtime.annotations.ConfigGroup annotation. These classes contain a field for each key within its group. In addition, configuration groups can be nested.

2.4.4.1. オプション設定グループ

A nested configuration group may be wrapped with an Optional type. In this case, the group is not populated unless one or more properties within that group are specified in the configuration. If the group is populated, then any required properties in the group must also be specified otherwise a configuration error will be reported and the application will not start.

2.4.5. 設定マップ

A Map can be used for configuration at any position where a configuration group would be allowed. The key type of such a map must be String, and its value may be either a configuration group class or a valid leaf type. The configuration key segment following the map’s key segment will be used as the key for map values.

2.4.6. 設定ルート

Configuration roots are configuration groups that appear in the root of the configuration tree. A configuration property’s full name is determined by joining the string quarkus. with the hyphenated name of the fields that form the path from the root to the leaf field. For example, if I define a configuration root group called ThreadPool, with a nested group in a field named sizing that in turn contains a field called minSize, the final configuration property will be called quarkus.thread-pool.sizing.min-size.

A configuration root’s name can be given with the name property, or it can be inferred from the class name. If the latter, then the configuration key will be the class name, minus any Config or Configuration suffix, broken up by camel-case, lowercased, and re-joined using hyphens (-).

A configuration root’s class name can contain an extra suffix segment for the case where there are configuration roots for multiple [Configuration Root Phases]. 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.

Note: The current implementation is still using injection site to determine the root set, so to avoid migration problems, it is recommended that the injection site (field or parameter) have the same name as the configuration root class until this change is complete.

2.4.6.1. 設定ルートフェーズ

Configuration roots are strictly bound by configuration phase, and attempting to access a configuration root from outside its corresponding phase will result in an error. A configuration root dictates when its contained keys are read from configuration, and when they are available to applications. The phases defined by io.quarkus.runtime.annotations.ConfigPhase are as follows:

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

BUILD_TIME

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

BUILD_AND_RUN_TIME_FIXED

Appropriate for things which affect build and must be visible for run time code. Not read from config at run time.

BOOTSTRAP

Used when runtime configuration needs to be obtained from an external system (like Consul), but details of that system need to be configurable (for example Consul’s URL). The high level way this works is by using the standard Quarkus config sources (such as properties files, system properties, etc.) and producing ConfigSourceProvider objects which are subsequently taken into account by Quarkus when creating the final runtime Config object.

RUN_TIME

Not available at build, read at start in all modes.

For all cases other than the BUILD_TIME case, the configuration root class and all the configuration groups and types contained therein must be located in, or reachable from, the extension’s run time artifact. Configuration roots of phase BUILD_TIME may be located in or reachable from either of the extension’s run time or deployment artifacts.

Bootstrap configuration steps are executed during runtime-init before any of other runtime steps. This means that code executed as part of this step cannot access anything that gets initialized in runtime init steps (runtime synthetic CDI beans being one such example).

2.4.7. 設定例

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.DefaultConverter

import java.io.File;
import java.util.logging.Level;

@ConfigGroup (1)
public class FileConfig {

    /**
     * Enable logging to a file.
     */
    @ConfigItem(defaultValue = "true")
    boolean enable;

    /**
     * The log format.
     */
    @ConfigItem(defaultValue = "%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.
     */
    @ConfigItem(defaultValue = "ALL")
    Level level;

    /**
     * The name of the file in which logs will be written.
     */
    @ConfigItem(defaultValue = "application.log")
    File path;

}

/**
 * Logging configuration.
 */
@ConfigRoot(phase = ConfigPhase.RUN_TIME) (2)
public class LogConfiguration {

    // ...

    /**
     * Configuration properties for the logging file handler.
     */
    FileConfig file;
}

public class LoggingProcessor {
    // ...

    /**
     * Logging configuration.
     */
    (3)
    LogConfiguration config;
}

A configuration property name can be split into segments. For example, a property name like quarkus.log.file.enable can be split into the following segments:

  • quarkus - a namespace claimed by Quarkus which is a prefix for all @ConfigRoot classes,

  • log - a name segment which corresponds to the LogConfiguration class annotated with @ConfigRoot,

  • file - a name segment which corresponds to the file field in this class,

  • enabled - a name segment which corresponds to enable field in FileConfig class annotated with @ConfigGroup.

1 The FileConfig class is annotated with @ConfigGroup to indicate that this is an aggregate configuration object containing a collection of configurable properties, rather than being a simple configuration key type.
2 The @ConfigRoot annotation indicates that this object is a configuration root group, in this case one which corresponds to a log segment. A class name is used to link configuration root group with the segment from a property name. The Configuration part is stripped off from a LogConfiguration class name and the remaining Log is lowercased to become a log. Since all @ConfigRoot annotated classes uses quarkus as a prefix, this finally becomes quarkus.log and represents the properties which names begin with quarkus.log.*.
3 Here the LoggingProcessor injects a LogConfiguration instance automatically by detecting the @ConfigRoot annotation.

A corresponding application.properties for the above example could be:

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

Since format is not defined in these properties, the default value from @ConfigItem will be used instead.

2.4.8. Enhanced conversion

You can use enhanced conversion of a config item by using the @ConvertWith annotation which accepts a Converter class object. If the annotation is present on a config item, the implicit or custom-built in converter in use will be overridden by the value provided. To do, see the example below which converts YES or NO values to boolean.

@ConfigRoot
public class SomeConfig {
    /**
     * Config item with enhanced converter
     */
    @ConvertWith(YesNoConverter.class) (1)
    @ConfigItem(defaultValue = "NO")
    Boolean answer;


    public static class YesNoConverter implements Converter<Boolean> {

        public YesNoConverter() {}

        @Override
        public Boolean convert(String s) {
            if (s == null || s.isEmpty()) {
                return false;
            }

            switch (s) {
                case "YES":
                    return true;
                case "NO":
                    return false;
            }

            throw new IllegalArgumentException("Unsupported value " + s + " given");
        }
    }
}
1 Override the default Boolean converter and use the provided converter which accepts a YES or NO config values.

The corresponding application.properties will look like.

quarkus.some.answer=YES

列挙型の値(設定項目)は、デフォルトではスキュード・ケース(ハイフン付き)に変換されます。以下の表は、列挙型名とその正規の同等表現を示しています。

Java enum 正規の同等表現

DISCARD

discard

READ_UNCOMMITTED

read-uncommitted

SIGUSR1

sigusr1

JavaEnum

java-enum

MAKING_LifeDifficult

making-life-difficult

YeOldeJBoss

ye-olde-jboss

camelCaseEnum

camel-case-enum

暗黙のコンバータまたはカスタム定義されたコンバータに基づくデフォルトの動作を使用するには、設定項目に @DefaultConverter アノテーションを追加します。

@ConfigRoot
public class SomeLogConfig {
    /**
     * The level of logs to be written into the file.
     */
    @DefaultConverter (1)
    @ConfigItem(defaultValue = "ALL")
    Level level;
}
1 Level.class enum を変換するには、デフォルトのコンバータ(内蔵またはカスタムコンバータ)を使用します。

2.5. Conditional Step Inclusion

It is possible to only include a given @BuildStep under certain conditions. The @BuildStep annotation has two optional parameters: onlyIf and onlyIfNot. These parameters can be set to one or more classes which implement BooleanSupplier. The build step will only be included when the method returns true (for onlyIf) or false (for onlyIfNot).

The condition class can inject configuration roots as long as they belong to a build-time phase. Run time configuration is not available for condition classes.

The condition class may also inject a value of type io.quarkus.runtime.LaunchMode. Constructor parameter and field injection is supported.

An example of a conditional build step
@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;
    }
}

If you need to make your build step conditional on the presence or absence of another extension, you can use Capabilities for that.

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");
}

When this build step is run nothing is printed to the console. This is because the HelloRecorder that is injected is actually a proxy that records all invocations. Instead, if we run the resulting Quarkus program we will see 'Hello World' printed to the console.

レコーダーのメソッドは値を返すことができますが、その値はプロキシ可能なものでなければなりません(プロキシ不可能なアイテムを返す場合は、 io.quarkus.runtime.RuntimeValue で囲みます)。これらのプロキシは、直接呼び出すことはできませんが、他のレコーダーのメソッドに渡すことができます。これは、他の @BuildStep メソッドも含めて、どのようなレコーダ・メソッドでもよいので、これらのレコーダの呼び出しの結果をラップした BuildItem インスタンスを生成するのが一般的なパターンです。

例えば、Servletの配置に任意の変更を加えるために、Undertowは ServletExtensionBuildItem を持っています。これは ServletExtension インスタンスをラップした MultiBuildItem です。他のモジュールのレコーダーから ServletExtension を返すと、Undertowはそれを使用し、Undertowを起動するレコーダー・メソッドに渡すことができます。

実行時には、生成された順にバイトコードが呼び出されます。つまり、ビルドステップの依存関係は、生成されたバイトコードが実行される順序を暗黙のうちに制御します。上の例では、 ServletExtensionBuildItem を生成するバイトコードが、それを消費するバイトコードよりも先に実行されることがわかっています。

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

  • プリミティブ

  • 文字列

  • Class<?> オブジェクト

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

  • Objects with a no-arg constructor and getter/setters for all properties (or public fields)

  • Objects with a constructor annotated with @RecordableConstructor with parameter names that match field names

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

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

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

Configuration objects with phase RUNTIME or BUILD_AND_RUNTIME_FIXED can be injected into recorders via constructor injection. Just create a constructor that takes the configuration objects the recorder needs. If the recorder has multiple constructors you can annotate the one you want Quarkus to use with @Inject. If the recorder wants to inject runtime config but is also used at static init time then it needs to inject a RuntimeValue<ConfigObject>, this value will only be set when the runtime methods are being invoked.

2.6.2. RecorderContext

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

与えられたクラス名で classProxy を呼び出すと、レコーダーのメソッドに渡すことができる Class が作成され、実行時には classProxy に渡された名前のクラスで置き換えられます。これは基本的に、レコーダで明示的にクラスをロードする必要性を避けるための便宜的なものです。

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

時には、アプリケーションを実行したときに、各スタートアップタスク(各バイトコードの記録結果)にかかる正確な時間を知りたい場合があります。この情報を確認する最も簡単な方法は、 -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のエクステンションは、さまざまな ビルド時拡張ポイントを利用することができます。

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 にあることになります。

Absolute paths are handled differently. If the above called route("/custom-endpoint"), the resulting endpoint will be found at /custom-endpoint.

エクステンションがネストした非アプリケーションのエンドポイントを必要とする場合:

@BuildStep
RouteBuildItem myNestedExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
    return nonApplicationRootPathBuildItem.routeBuilder()
                .nestedRoute("custom-endpoint", "deep")
                .handler(new MyCustomHandler())
                .displayOnNotFoundPage()
                .build();
}

Given a default non-application endpoint root of /q, this will create an endpoint at /q/custom-endpoint/deep.

Absolute paths also have an impact on nested endpoints. If the above called nestedRoute("custom-endpoint", "/deep"), the resulting endpoint will be found at /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 ヘルスチェックを オプションの 依存関係としてランタイムモジュールにインポートすることで、ヘルスチェックが含まれていない場合でもアプリケーションのサイズに影響を与えないようにします。

  • Create your health check following the SmallRye Health guide. We advise providing only readiness check for an extension (liveness check is designed to express the fact that an application is up and needs to be lightweight).

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

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

  • Add a way to disable the extension health check via a config item quarkus.<extension>.health.enabled that should be enabled by default.

Following is an example from the Agroal extension that provides a DataSourceHealthCheck to validate the readiness of a datasource.

@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 の互換性レイヤは、将来的には別のエクステンションに移行する予定です。

There are two broad patterns that extensions can use to interact with an optional metrics extension to add their own metrics:

  • Consumer pattern: An extension declares a MetricsFactoryConsumerBuildItem and uses that to provide a bytecode recorder to the metrics extension. When the metrics extension has initialized, it will iterate over registered consumers to initialize them with a MetricsFactory. This factory can be used to declare API-agnostic metrics, which can be a good fit for extensions that provide an instrumentable object for gathering statistics (e.g. Hibernate’s Statistics class).

  • Binder pattern: An extension can opt to use completely different gathering implementations depending on the metrics system. An Optional<MetricsCapabilityBuildItem> metricsCapability build step parameter can be used to declare or otherwise initialize API-specific metrics based on the active metrics extension (e.g. "smallrye-metrics" or "micrometer"). This pattern can be combined with the consumer pattern by using MetricsFactory::metricsSystemSupported() to test the active metrics extension within the recorder.

Remember that support for metrics is optional. Extensions can use an Optional<MetricsCapabilityBuildItem> metricsCapability parameter in their build step to test for the presence of an enabled metrics extension. Consider using additional configuration to control behavior of metrics. Datasource metrics can be expensive, for example, so additional configuration flags are used enable metrics collection on individual datasources.

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

  1. An underlying library used by the extension is using a specific Metrics API directly (either MP Metrics, Micrometer, or some other).

  2. An underlying library uses its own mechanism for collecting metrics and makes them available at runtime using its own API, e.g. Hibernate’s Statistics class, or Vert.x MetricsOptions.

  3. An underlying library does not provide metrics (or there is no library at all) and you want to add instrumentation.

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

If the library directly uses a metrics API, there are two options:

  • Use an Optional<MetricsCapabilityBuildItem> metricsCapability parameter to test which metrics API is supported (e.g. "smallrye-metrics" or "micrometer") in your build step, and use that to selectively declare or initialize API-specific beans or build items.

  • Create a separate build step that consumes a MetricsFactory, and use the MetricsFactory::metricsSystemSupported() method within the bytecode recorder to initialize required resources if the desired metrics API is supported (e.g. "smallrye-metrics" or "micrometer").

Extensions may need to provide a fallback if there is no active metrics extension or the extension doesn’t support the API required by the library.

2.11.2. Case 2: The library provides its own metric API

There are two examples of a library providing its own metrics API:

  • The extension defines an instrumentable object as Agroal does with io.agroal.api.AgroalDataSourceMetrics, or

  • The extension provides its own abstraction of metrics, as Jaeger does with io.jaegertracing.spi.MetricsFactory.

2.11.2.1. Observing instrumentable objects

Let’s take the instrumentable object (io.agroal.api.AgroalDataSourceMetrics) case first. In this case, you can do the following:

  • Define a BuildStep that produces a MetricsFactoryConsumerBuildItem that uses a RUNTIME_INIT or STATIC_INIT Recorder to define a MetricsFactory consumer. For example, the following creates a MetricsFactoryConsumerBuildItem if and only if metrics are enabled both for Agroal generally, and for a datasource specifically:

    @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())));
            }
        }
    }
  • The associated recorder should use the provided MetricsFactory to register metrics. For Agroal, this means using the MetricFactory API to observe io.agroal.api.AgroalDataSourceMetrics methods. For example:

    /* 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);
                ....

The MetricsFactory provides a fluid builder for registration of metrics, with the final step constructing gauges or counters based on a Supplier or ToDoubleFunction. Timers can either wrap Callable, Runnable, or Supplier implementations, or can use a TimeRecorder to accumulate chunks of time. The underlying metrics extension will create appropriate artifacts to observe or measure the defined functions.

2.11.2.2. Using a Metrics API-specific implementation

Using metrics-API specific implementations may be preferred in some cases. Jaeger, for example, defines its own metrics interface, io.jaegertracing.spi.MetricsFactory, that it uses to define counters and gauges. A direct mapping from that interface to the metrics system will be the most efficient. In this case, it is important to isolate these specialized implementations and to avoid eager classloading to ensure the metrics API remains an optional, compile-time dependency.

Optional<MetricsCapabilityBuildItem> metricsCapability can be used in the build step to selectively control initialization of beans or the production of other build items. The Jaeger extension, for example, can use the following to control initialization of specialized Metrics API adapters:

+

/* 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);
        }
    }
}

A recorder consuming a MetricsFactory can use MetricsFactory::metricsSystemSupported() can be used to control initialization of metrics objects during bytecode recording in a similar way.

2.11.3. ケース3:エクステンションコード内でメトリクスを収集する必要がある

To define your own metrics from scratch, you have two basic options: Use the generic MetricFactory builders, or follow the binder pattern, and create instrumentation specific to the enabled metrics extension.

To use the extension-agnostic MetricFactory API, your processor can define a BuildStep that produces a MetricsFactoryConsumerBuildItem that uses a RUNTIME_INIT or STATIC_INIT Recorder to define a MetricsFactory consumer.

+

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
MetricsFactoryConsumerBuildItem registerMetrics(MyExtensionRecorder recorder) {
    return new MetricsFactoryConsumerBuildItem(recorder.registerMetrics());
}

+ - The associated recorder should use the provided MetricsFactory to register metrics, for example

+

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);
            ....

Remember that metrics extensions are optional. Keep metrics-related initialization isolated from other setup for your extension, and structure your code to avoid eager imports of metrics APIs. Gathering metrics can also be expensive. Consider using additional extension-specific configuration to control behavior of metrics if the presence/absence of metrics support isn’t sufficient.

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

Add a dependency to quarkus-jackson-spi on your extension’s deployment module.

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jackson-spi</artifactId>
</dependency>

Add a build step to your processor to register a Jackson module via the JacksonModuleBuildItem. You need to name your module in a unique way across all Jackson modules.

@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. Customizing JSON-B

First, add an optional dependency to quarkus-jsonb on your extension’s runtime module.

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jsonb</artifactId>
  <optional>true</optional>
</dependency>

Then create a serializer and/or a deserializer for JSON-B, an example of which can be seen in the mongodb-panache extension.

public class ObjectIdSerializer implements JsonbSerializer<ObjectId> {
    @Override
    public void serialize(ObjectId obj, JsonGenerator generator, SerializationContext ctx) {
        if (obj != null) {
            generator.write(obj.toString());
        }
    }
}

Add a dependency to quarkus-jsonb-spi on your extension’s deployment module.

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jsonb-spi</artifactId>
</dependency>

Add a build step to your processor to register the serializer via the JsonbSerializerBuildItem.

@BuildStep
JsonbSerializerBuildItem registerJsonbSerializer() {
    return new JsonbSerializerBuildItem(io.quarkus.mongodb.panache.jsonb.ObjectIdSerializer.class.getName()));
}

The JSON-B extension will then use the produced build item to register your serializer/deserializer automatically.

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. ライブリロードのトリガー

Live reload is generally triggered by an HTTP request, however not all applications are HTTP applications and some extensions may want to trigger live reload based on other events. To do this you need to implement io.quarkus.dev.spi.HotReplacementSetup in your runtime module, and add a META-INF/services/io.quarkus.dev.spi.HotReplacementSetup that lists your implementation.

起動時には 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 javax.enterprise.inject.Instance;
import javax.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 This producer is used to build the application to be tested. It uses Shrinkwrap to create a JavaArchive to test
3 It is possible to inject beans from our test deployment directly into the test case
4 This method directly invokes the health check Servlet and verifies the response
5 This method uses the injected health check bean to verify it is returning the expected result

If you want to test that an extension properly fails at build time, use the setExpectedException method:

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 This starts the deployment, your test can modify it as part of the test suite. Quarkus will be restarted between each test method so every method starts with a clean deployment.
2 This method allows you to modify the source of a class file. The old source is passed into the function, and the updated source is returned.
3 This method adds a new class file to the deployment. The source that is used will be the original source that is part of the current project.
4 このメソッドは静的リソースを変更します
5 このメソッドは新しい静的リソースを追加します

2.16. ネイティブ実行可能ファイルのサポート

There Quarkus provides a lot of build items that control aspects of the native executable build. This allows for extensions to programmatically perform tasks such as registering classes for reflection or adding static resources to the native executable. Some of these build items are listed below:

io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem

ネイティブ実行可能ファイルに静的なリソースをインクルードします。

io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem

ディレクトリの静的リソースをネイティブ実行可能ファイルにインクルードします。

io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem

A class that will be reinitialized at runtime by Substrate. This will result in the static initializer running twice.

io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem

ネイティブ実行可能ファイルのビルド時に設定されるシステムプロパティです。

io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem

Includes a resource bundle in the native executable.

io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem

Registers a class for reflection in Substrate. Constructors are always registered, while methods and fields are optional.

io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem

A class that will be initialized at runtime rather than build time. This will cause the build to fail if the class is initialized as part of the native executable build process, so care must be taken.

io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem

A convenience feature that allows you to control most of the above features from a single build item.

io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem

ネイティブイメージですべての文字セットを有効にすることを示します。

io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem

A convenient way to tell Quarkus that the extension requires SSL, and it should be enabled during native image build. When using this feature, remember to add your extension to the list of extensions that offer SSL support automatically on the native and ssl guide.

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.quiltflower.enabled property to true then Quarkus will download and invoke the Quiltflower 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
The property value could be either an absolute path, such as /home/foo/dump on a Linux machine, or a path relative to the user working directory, i.e. dump corresponds to the {user.dir}/target/dump in the dev mode and {user.dir}/dump when running the tests.

You should see a line in the log for each class written to the directory:

INFO  [io.qua.run.boo.StartupActionImpl] (main) Wrote /path/to/my/app/target/dump-classes/io/quarkus/arc/impl/ActivateRequestContextInterceptor_Bean.class

The property is also honored when running tests:

./mvnw clean test -Dquarkus.debug.generated-classes-dir=target/dump-generated-classes

Analogously, you can use the quarkus.debug.transformed-classes-dir and quarkus.debug.transformed-classes-dir properties to dump the relevant output.

2.18.2. マルチモジュールのMavenプロジェクトと開発モード

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

./mvnw compile quarkus:dev -DnoDeps

2.18.3. Indexer does not include your external dependency

Remember to add IndexDependencyBuildItem artifacts to your @BuildStep.

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

We have an extension that is used to test for regressions in the extension processing. It is located in https://github.com/quarkusio/quarkus/tree/main/integration-tests/test-extension/extension directory. In this section we touch on some tasks an extension author will typically need to perform using the test-extension code to illustrate how the task could be done.

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

フィーチャーは、 FeatureBuildItem を生成する link:#Build Step Processors[[Build Step Processors]]メソッドで登録することができます。

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 resteasy-reactive-jackson feature is referenced:

mvn io.quarkus.platform:quarkus-maven-plugin:2.11.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json \
    -DclassName="org.acme.rest.json.FruitResource" \
    -Dpath="/fruits" \
    -Dextensions="resteasy-reactive,resteasy-reactive-jackson"
cd rest-json
2.19.1.2. Capabilities

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

ケイパビリティは、 CapabilityBuildItem を生成する link:#Build Step Processors[[Build Step Processors]]メソッドで登録することができます。

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...
        }
    }

Capabilities should follow the naming conventions of Java packages; e.g. io.quarkus.security.jpa. Capabilities provided by core extensions should be listed in the io.quarkus.deployment.Capability enum and their name should always start with the io.quarkus prefix.

2.19.2. Bean Defining Annotations

The CDI layer processes CDI beans that are either explicitly registered or that it discovers based on bean defining annotations as defined in 2.5.1. Bean defining annotations. You can expand this set of annotations to include annotations your extension processes using a BeanDefiningAnnotationBuildItem as shown in this TestProcessor#registerBeanDefinningAnnotations example:

Register a Bean Defining Annotation
import javax.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 Register the annotation class and CDI default scope using the Jandex DotName class.
2 ConfiguredBean will be processed by the CDI layer the same as a bean annotated with the CDI standard @ApplicationScoped.

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 Look for a config.xml classpath resource
2 If found, parse using JAXB context for XmlConfig.class

If there was no /config.xml resource available in the build environment, then a null RuntimeServiceBuildItem would be returned and no subsequent logic based on a RuntimeServiceBuildItem being produced would execute.

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 [Manage Non-CDI Service] section.

If for some reason you need to parse the config and use it in other build steps in an extension processor, you would need to create an XmlConfigBuildItem to pass the parsed XmlConfig instance around.

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 [Replacing Classes in the Native Image].

2.19.4. Scanning Deployments Using Jandex

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

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 Depend on a BeanArchiveIndexBuildItem to have the build step be run after the deployment has been indexed.
2 Retrieve the index.
3 Find all beans annotated with @TestAnnotation.
4 Determine which of these beans also has the IConfigConsumer interface.
5 Save the bean class in a TestBeanBuildItem for use in a latter RUNTIME_INIT build step that will interact with the bean instances.

2.19.5. Interacting With Extension Beans

You can use the io.quarkus.arc.runtime.BeanContainer interface to interact with your extension beans. The following configureBeans methods illustrate interacting with the beans scanned for in the previous section:

Using CDI BeanContainer Interface
// 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.instance(beanClass); (4)
        instance.loadConfig(buildTimeConfig, runTimeConfig); (5)
        log.infof("configureBeans, instance=%s\n", instance);
    }
1 Consume the `TestBeanBuildItem`s produced from the scanning build step.
2 Consume the BeanContainerBuildItem to order this build step to run after the CDI bean container has been created.
3 Call the runtime recorder to record the bean interactions.
4 Runtime recorder retrieves the bean using its type.
5 Runtime recorder invokes the IConfigConsumer#loadConfig(…​) method passing in the configuration objects with runtime information.

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 [Parsing Config to Objects]. 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 Call into the runtime recorder to record the creation of the service.
2 Using the parsed XmlConfig instance, create an instance of RuntimeXmlConfigService and wrap it in a RuntimeValue. Use a RuntimeValue wrapper for non-interface objects that are non-proxiable.
3 Wrap the return service value in a RuntimeServiceBuildItem for use in a RUNTIME_INIT build step that will start the service.
2.19.6.1. サービスの開始

Now that you have recorded the creation of a service during the build phase, you need to record how to start the service at runtime during booting. You do this with a RUNTIME_INIT build step as shown in the TestProcessor#startRuntimeService method.

非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 We consume the previously initialized service captured in RuntimeServiceBuildItem.
3 Call the runtime recorder to record the service start invocation.
4 Produce a ServiceStartBuildItem to indicate the startup of a service. See [Startup and Shutdown Events] for details.
5 Runtime recorder retrieves the service instance reference and calls its startService method.
6 Runtime recorder registers an invocation of the service instance stopService method with the Quarkus ShutdownContext.

The code for the RuntimeXmlConfigService can be viewed here: {quarkus-blob-url}/integration-tests/test-extension/extension/runtime/src/main/java/io/quarkus/extest/runtime/RuntimeXmlConfigService.java[RuntimeXmlConfigService.java]

The testcase for validating that the RuntimeXmlConfigService has started can be found in the testRuntimeXmlConfigService test of ConfiguredBeanTest and NativeImageIT.

2.19.7. スタートアップとシャットダウンのイベント

The Quarkus container supports startup and shutdown lifecycle events to notify components of the container startup and shutdown. There are CDI events fired that components can observe are illustrated in this example:

コンテナの起動を観察
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 Observe a StartupEvent to be notified the runtime has started.
2 Observe a ShutdownEvent to be notified when the runtime is going to shut down.

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 [Starting a Service] section. These shutdown tasks would be called after a ShutdownEvent had been sent.

A StartupEvent is fired after all io.quarkus.deployment.builditem.ServiceStartBuildItem producers have been consumed. The implication of this is that if an extension has services that application components would expect to have been started when they observe a StartupEvent, the build steps that invoke the runtime code to start those services needs to produce a ServiceStartBuildItem to ensure that the runtime code is run before the StartupEvent is sent. Recall that we saw the production of a ServiceStartBuildItem in the previous section, and it is repeated here for clarity:

Example of Producing a ServiceStartBuildItem
// TestProcessor#startRuntimeService
    @BuildStep
    @Record(RUNTIME_INIT)
    ServiceStartBuildItem startRuntimeService(TestRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem,
            RuntimeServiceBuildItem serviceBuildItem) throws IOException {
...
        return new ServiceStartBuildItem("RuntimeXmlConfigService"); (1)
    }
1 Produce a ServiceStartBuildItem to indicate that this is a service starting step that needs to run before the StartupEvent is sent.

2.19.8. Register Resources for Use in Native Image

Not all configuration or resources can be consumed at build time. If you have classpath resources that the runtime needs to access, you need to inform the build phase that these resources need to be copied into the native image. This is done by producing one or more NativeImageResourceBuildItem or NativeImageResourceBundleBuildItem in the case of resource bundles. Examples of this are shown in this sample registerNativeImageResources build step:

Registering Resources and ResourceBundles
public final class MyExtProcessor {
    @Inject
    BuildProducer<NativeImageResourceBuildItem> resource;
    @Inject
    BuildProducer<NativeImageResourceBundleBuildItem> resourceBundle;

    @BuildStep
    void registerNativeImageResources() {
        resource.produce(new NativeImageResourceBuildItem("/security/runtime.keys")); (1)

        resource.produce(new NativeImageResourceBuildItem(
                "META-INF/my-descriptor.xml")); (2)

        resourceBundle.produce(new NativeImageResourceBuildItem("javax.xml.bind.Messages")); (3)
    }
}
1 Indicate that the /security/runtime.keys classpath resource should be copied into native image.
2 Indicate that the META-INF/my-descriptor.xml resource should be copied into native image
3 Indicate that the "javax.xml.bind.Messages" resource bundle should be copied into native image.

2.19.9. Service files

If you are using META-INF/services files you need to register the files as resources so that your native image can find them, but you also need to register each listed class for reflection so they can be instantiated or inspected at run-time:

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 takes a list of service implementation classes as parameters: if you are not reading them from the service file, make sure that they correspond to the service file contents because the service file will still be read and used at run-time. This is not a substitute for writing a service file.
This only registers the implementation classes for instantiation via reflection (you will not be able to inspect its fields and methods). If you need to do that, you can do it this way:
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])));
    }
}

While this is the easiest way to get your services running natively, it’s less efficient than scanning the implementation classes at build time and generating code that registers them at static-init time instead of relying on reflection.

You can achieve that by adapting the previous build step to use a static-init recorder instead of registering classes for reflection:

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. オブジェクトの置換

Objects created during the build phase that are passed into the runtime need to have a default constructor in order for them to be created and configured at startup of the runtime from the build time state. If an object does not have a default constructor you will see an error similar to the following during generation of the augmented artifacts:

DSAPublicKey Serialization Error
	[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

There is a io.quarkus.runtime.ObjectSubstitution interface that can be implemented to tell Quarkus how to handle such classes. An example implementation for the DSAPublicKey is shown here:

DSAPublicKeyObjectSubstitution Example
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 The serialize method takes the object without a default constructor and creates a KeyProxy that contains the information necessary to recreate the DSAPublicKey.
2 The deserialize method uses the KeyProxy to recreate the DSAPublicKey from its encoded form using the key factory.

An extension registers this substitution by producing an ObjectSubstitutionBuildItem as shown in this TestProcessor#loadDSAPublicKey fragment:

オブジェクト置換の登録
    @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 アノテーションに依存しないバージョンのクラスに置き換える方法の例を以下に示します。

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. 設定リファレンスドキュメント

設定は各エクステンションの重要な部分なので、適切に文書化する必要があります。それぞれの設定プロパティには、適切な Javadoc コメントが必要です。

While it is handy to have the documentation available when coding, this configuration documentation must also be available in the extension guides. The Quarkus build automatically generates the configuration documentation for you based on the Javadoc comments, but you need to explicitly include it in your guide.

このセクションでは、設定リファレンスドキュメントについて知っておくべきことをすべて説明します。

3.1. ドキュメントの書き方

それぞれの設定プロパティに対して、その目的を説明するJavadocを書く必要があります。

サマリーテーブルに記載されるので、必ず最初の一文に意味を持たせ、自己完結させましょう。

標準のJavadocコメントかAsciidocを直接Javadocコメントとして使うことができます。

ここでは、Javadocのコメントを書くことに慣れていることを前提としているので、Asciidocのサポートに焦点を当ててみましょう。標準的なJavadocのコメントは単純な文書化には完全に適していますが(推奨されています)、もしあなたがTipsやソースコードの抜粋、リストなどを含めたい場合は、Asciidocが便利です。

以下は、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
 */
@ConfigItem
public Optional<String> dialect;

This is the simple case: you just have to write Asciidoc and mark the comment with the @asciidoclet tag. This tag has two purposes: it is used as a marker for our generation tool, but it is also used by the javadoc process for proper Javadoc generation.

では、もう少し複雑な例を考えてみましょう。

// @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
@ConfigItem
public Optional<String> sqlLoadScript;

これについてのコメントをいくつか。

  • Javadocのコメントでインデントを尊重する必要がある場合(複数行にまたがるリスト項目やインデントされたソースコードを考えてみてください)には、Eclipseの自動フォーマッタを一時的に無効にする必要があります(これはEclipseを使用していない場合でも同様です。私たちのビルドにはフォーマッタが含まれている為です)。これを行うには、 // @formatter:off / // @formatter:on マーカーを使用してください。これらは別々のコメントであり、 // マーカーの後にスペースがあることに注意してください。これは必須です。

  • 見ての通り、Asciidoctorのフルパワーが使えます(以下の制限を除く)

Asciidoctor のドキュメントでは、オープンブロック ( -- ) を使用することはできません。他のすべてのタイプのブロック (ソース、アドミッション…​) がサポートされています。

By default, the doc generator will use the hyphenated field name as the key of a java.util.Map configuration item. To override this default and have a user-friendly key (independent of implementation details), you may use the io.quarkus.runtime.annotations.ConfigDocMapKey annotation. See the following example,

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

3.2. セクションのドキュメントを書く

ある @ConfigGroup の設定セクションを生成したい場合は、 @ConfigDocSection アノテーションで対応できます。以下のコード例を参照してください。

/**
* Config group related configuration.
* Amazing introduction here
*/
@ConfigItem
@ConfigDocSection (1)
public ConfigGroupConfig configGroup;
1 This will add a section documentation for the configGroup config item in the generated documentation. Section’s title and introduction will be derived from the javadoc of the configuration item. The first sentence from the javadoc is considered as the section title and the remaining sentences used as section introduction. You can also use the @asciidoclet tag as shown above.

3.3. ドキュメントの生成

ドキュメントの生成は簡単です。

  • ./mvnw clean install -DskipTests -DskipITs を実行すればOKです。

  • グローバルに行うことも、特定のエクステンションディレクトリ ( extensions/mailer など) で行うこともできます。

ドキュメントはプロジェクトのルートにあるグローバル target/asciidoc/generated/config/ で生成されます。

3.4. エクステンションガイドにドキュメントを含める

これで、あなたのエクステンションの設定参照ドキュメントが生成されたので、それをガイドに含める必要があります (そして、それをレビューする必要があります)。

これは簡単で、ガイドで生成されたドキュメントを含めます。

include::{generated-dir}/config/quarkus-your-extension.adoc[opts=optional, leveloffset=+1]

設定グループのために生成されたドキュメントを含めたい場合は、以下のinclude文を使用することができます。

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 がある場合は、以下のようにすればよいでしょう。

まず、抽出した application.properties のすぐ下にチップを入れます。

[TIP]
For more information about the extension configuration please refer to the <<configuration-reference, Configuration Reference>>.

そして、文書の最後には、エクステンションのドキュメントをインクルードしてください。

[[configuration-reference]]
== 設定リファレンス

include::{generated-dir}/config/quarkus-your-extension.adoc[opts=optional, leveloffset=+1]

最後にドキュメントを生成して確認します。

4. エクステンションの継続的なテスト

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

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

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

  • The quarkus-extension.yaml file (in the extension’s runtime/ module) has the minimum metadata set:

    • name

    • description (unless you have it already set in the runtime/pom.xml's <description> element, which is the recommended approach)

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

  • エクステンションのリポジトリは、 Ecosystem CIを使用するように設定されていること

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

以上で完了です。プルリクエストがマージされると、スケジュールされたジョブがMaven Centralの新しいバージョンをチェックし、 link:extension-registry-user.html[Quarkus Extension Registry]をアップデートします。