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

REST クライアントの利用

このガイドでは、REST APIと対話するためのREST Clientの使用方法について説明します。 RESTクライアントは、Quarkus REST(旧RESTEasy Reactive)と互換性のあるRESTクライアントの実装です。

アプリケーションでクライアントを使用し、RESTエンドポイントを公開する場合は、サーバー部分に Quarkus REST を使用してください。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 17+がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.9.6

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

git clone https://github.com/quarkusio/quarkus-quickstarts.git で Git レポジトリをクローンするか、 アーカイブ をダウンロードします。

ソリューションは rest-client-quickstart ディレクトリ にあります。

Maven プロジェクトの作成

最初に、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します:

コマンドラインインタフェース
quarkus create app org.acme:rest-client-quickstart \
    --extension='rest-jackson,rest-client-jackson' \
    --no-code
cd rest-client-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストールと使用方法の詳細については、 Quarkus CLI ガイドを参照してください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.9.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-client-quickstart \
    -Dextensions='rest-jackson,rest-client-jackson' \
    -DnoCode
cd rest-client-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

Windowsユーザーの場合:

  • cmdを使用する場合、(バックスラッシュ \ を使用せず、すべてを同じ行に書かないでください)。

  • Powershellを使用する場合は、 -D パラメータを二重引用符で囲んでください。例: "-DprojectArtifactId=rest-client-quickstart"

このコマンドは、 REST エンドポイントを持つ Maven プロジェクトを生成し、以下をインポートします。

  • REST サーバーのサポートには、 rest-jackson エクステンションを使用してください。Jackson を使用したくない場合は、代わりに rest を使用してください;

  • REST クライアントのサポートには rest-client-jackson エクステンションを使用してください。Jackson を使用したくない場合は、代わりに rest-client を使用してください。

Quarkusプロジェクトがすでに設定されている場合は、プロジェクトのベースディレクトリで次のコマンドを実行することで、 rest-client-jackson エクステンションをプロジェクトに追加できます:

コマンドラインインタフェース
quarkus extension add rest-client-reactive-jackson
Maven
./mvnw quarkus:add-extension -Dextensions='rest-client-reactive-jackson'
Gradle
./gradlew addExtension --extensions='rest-client-reactive-jackson'

これにより、以下がビルドファイルに追加されます。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-rest-client-jackson")

モデルの作成

このガイドでは、 stage.code.quarkus.io サービスが提供する REST API の一部を利用する方法をデモします。まず最初に、使用するモデルを Extension POJO の形式でセットアップします。

以下の内容で src/main/java/org/acme/rest/client/Extension.java ファイルを作成します。

package org.acme.rest.client;

import java.util.List;

public class Extension {

    public String id;
    public String name;
    public String shortName;
    public List<String> keywords;

}

上記のモデルは、サービスによって提供されるフィールドのサブセットに過ぎませんが、このガイドの目的には十分です。

インターフェースの作成

REST クライアントの使用は、適切な Jakarta REST および MicroProfile アノテーションを使用してインターフェースを作成するのと同じくらい簡単です。今回の場合、インターフェースは src/main/java/org/acme/rest/client/ExtensionsService.java で作成し、以下の内容を持つ必要があります:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;

@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);
}

getById メソッドは、 ID を指定して Code Quarkus API からエクステンションを取得する機能を提供します。クライアントがすべてのネットワークとマーシャリングを処理するため、コードにそのような技術的な詳細を記述する必要はありません。

上記のコードのアノテーションの目的は以下の通りです。

  • @RegisterRestClient により、Quarkusは、このインターフェイスがRESTクライアントとしてCDIインジェクションに利用可能であることを知ることができます。

  • @Path@GET@QueryParam は、サービスへのアクセス方法を定義するために使用される標準的な Jakarta REST アノテーションです。

quarkus-rest-client-jackson エクステンションがインストールされている場合、メディアタイプが @Produces または @Consumes アノテーションによって明示的に設定されていない限り、Quarkusはほとんどの返り値に対してデフォルトで application/json メディアタイプを使用します。

JSONのデフォルトに依存しない場合は、エンドポイントに @Produces@Consumes のアノテーションを付けて、期待されるcontent-typesを正確に定義することが強く推奨されています。これにより、ネイティブ実行可能ファイルに含まれる Jakarta REST プロバイダー(コンバーターと見なすことができます)の数を減らすことができます。

上記の getById メソッドはブロッキング呼び出しです。イベントループで呼び出さないでください。非同期のサポート セクションでは、非ブロッキング呼び出しを行う方法について説明します。

クエリーパラメーター

クエリーパラメーターを指定する最も簡単な方法は、クライアントメソッドのパラメーターに @QueryParam または @RestQuery というアノテーションを付けることです。 @RestQuery@QueryParam と同等ですが、名前を省略することができます。さらに、クエリーパラメーターを Map として渡すこともでき、事前にパラメーターがわからない場合に便利です。

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.RestQuery;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.Map;
import java.util.Set;

@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);

    @GET
    Set<Extension> getByName(@RestQuery String name); (1)

    @GET
    Set<Extension> getByFilter(@RestQuery Map<String, String> filter); (2)

    @GET
    Set<Extension> getByFilters(@RestQuery MultivaluedMap<String, String> filters); (3)

}
1 リクエストクエリーには name をキーとするパラメーターが含まれます
2 Map エントリは、ちょうど 1 つのクエリーパラメーターを表します
3 MultivaluedMap は配列の値を送信することができます

@ClientQueryParam を使用

リクエストにクエリパラメータを追加するもうひとつの方法は、 REST クライアントインターフェースあるいはそのインターフェースの特定のメソッドに @io.quarkus.rest.client.reactive.ClientQueryParam を使用することです。アノテーションではクエリパラメータ名を指定し、 値は定数や設定プロパティ、あるいはメソッドの起動時に決定できます。

次の例では、さまざまな使い方を紹介します:

@ClientQueryParam(name = "my-param", value = "${my.property-value}") (1)
public interface Client {
    @GET
    String getWithParam();

    @GET
    @ClientQueryParam(name = "some-other-param", value = "other") (2)
    String getWithOtherParam();

    @GET
    @ClientQueryParam(name = "param-from-method", value = "{with-param}") (3)
    String getFromMethod();

    default String withParam(String name) {
        if ("param-from-method".equals(name)) {
            return "test";
        }
        throw new IllegalArgumentException();
    }
}
1 @ClientQueryParam をインターフェイスに付与することで、クライアントのすべてのリクエストに my-param が追加されることを保証しています。 なぜなら、${…​} 構文を使用したため、パラメータの実際の値は my.property-value 設定プロパティを使用して取得されます。
2 getWithOtherParam を呼び出すと、クエリーパラメータ my-param に加え、 some-other-paramother の値が追加されます。
3 getFromMethod を呼び出すと、 my-param のクエリパラメータに加えて、 param-from-methodtest の値 ( withParam のメソッドが param-from-method と呼び出されたときに返すため) が追加されます。

インターフェースメソッドに @QueryParam のアノテーションが付いた引数がある場合、その引数は @ClientQueryParam のアノテーションで指定されたものよりも優先されることに注意してください。

More information about this annotation can be found on the javadoc of @ClientQueryParam.

フォームパラメータ

Form parameters can be specified using @RestForm (or @FormParam) annotations:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.RestForm;

import jakarta.ws.rs.PORT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.Map;
import java.util.Set;

@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    Set<Extension> postId(@FormParam("id") String id);

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    Set<Extension> postName(@RestForm String name);

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    Set<Extension> postFilter(@RestForm Map<String, String> filter);

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    Set<Extension> postFilters(@RestForm MultivaluedMap<String, String> filters);

}

@ClientFormParamの使用

フォームパラメータは、 @ClientQueryParam と同様に @ClientFormParam を使用して指定することもできます:

@ClientFormParam(name = "my-param", value = "${my.property-value}")
public interface Client {
    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    String postWithParam();

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @ClientFormParam(name = "some-other-param", value = "other")
    String postWithOtherParam();

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @ClientFormParam(name = "param-from-method", value = "{with-param}")
    String postFromMethod();

    default String withParam(String name) {
        if ("param-from-method".equals(name)) {
            return "test";
        }
        throw new IllegalArgumentException();
    }
}

このアノテーションの詳細は @ClientFormParam のjavadocで確認できます。

パスパラメーター

GET リクエストにパスパラメーターが必要な場合は、@QueryParam の代わりに (あるいは加えて) @PathParam ("parameter-name") アノテーションを使用することができます。パスパラメーターとクエリーパラメーターは、必要に応じて以下の例のように組み合わせることができます。

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
import java.util.Set;

@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {

    @GET
    @Path("/stream/{stream}")
    Set<Extension> getByStream(@PathParam("stream") String stream, @QueryParam("id") String id);
}

大きなペイロードの送信

The REST Client is capable of sending arbitrarily large HTTP bodies without buffering the contents in memory, if one of the following types is used:

  • InputStream

  • Multi<io.vertx.mutiny.core.buffer.Buffer>

Furthermore, the client can also send arbitrarily large files if one of the following types is used:

  • File

  • Path

設定の作成

REST 呼び出しが行われるベース URL を決定するために、REST クライアントは application.properties からの設定を使用します。プロパティーの名前は、以下のコードで表示される特定の規則に従う必要があります。

# Your configuration properties
quarkus.rest-client."org.acme.rest.client.ExtensionsService".url=https://stage.code.quarkus.io/api # (1)
1 Having this configuration means that all requests performed using org.acme.rest.client.ExtensionsService will use https://stage.code.quarkus.io/api as the base URL. Using the configuration above, calling the getById method of ExtensionsService with a value of io.quarkus:quarkus-rest-client would result in an HTTP GET request being made to https://stage.code.quarkus.io/api/extensions?id=io.quarkus:quarkus-rest-client.

org.acme.rest.client.ExtensionsService は、前のセクションで作成した ExtensionsService インターフェースの完全修飾名と 一致しなければならない ことに注意してください。

設定を容易にするために、@RegisterRestClient configKey プロパティーを使用して、インターフェイスの完全修飾名とは別の設定ルートを使用することができます。

@RegisterRestClient(configKey="extensions-api")
public interface ExtensionsService {
    [...]
}
# Your configuration properties
quarkus.rest-client.extensions-api.url=https://stage.code.quarkus.io/api
quarkus.rest-client.extensions-api.scope=jakarta.inject.Singleton

ホスト名の検証の無効化

特定の REST クライアントの SSL ホスト名検証を無効にするには、以下の内容を設定に追加します。

quarkus.rest-client.extensions-api.verify-host=false

この設定は、あらゆる種類の SSL ホスト名検証を無効にするため、実運用環境では使用しないでください。

HTTP/2 サポート

REST Clientでは、HTTP/2はデフォルトで無効になっています。有効にしたい場合は、次のように設定することができます:

// for all REST Clients:
quarkus.rest-client.http2=true
// or for a single REST Client:
quarkus.rest-client.extensions-api.http2=true

または、Application-Layer Protocol Negotiation (alpn) TLSエクステンションを有効にすると、クライアントは、サーバーが互換性のあるものよりも、どのHTTPバージョンを使用するかをネゴシエートすることができます。デフォルトでは、まずHTTP/2を使用しようとし、有効になっていない場合はHTTP/1.1を使用します。有効にしたい場合は、以下のように設定します:

quarkus.rest-client.alpn=true
// or for a single REST Client:
quarkus.rest-client.extensions-api.alpn=true

Jakarta RESTリソースの作成

以下の内容の src/main/java/org/acme/rest/client/ExtensionsResource.java ファイルを作成します。

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Set;

@Path("/extension")
public class ExtensionsResource {

    @RestClient (1)
    ExtensionsService extensionsService;


    @GET
    @Path("/id/{id}")
    public Set<Extension> id(String id) {
        return extensionsService.getById(id);
    }
}

このリストには 2 つの興味深い部分があります。

1 クライアントスタブには、通常の CDI @Inject ではなく、@RestClient アノテーションが挿入されます。

QuarkusRestClientBuilderを使用したプログラムによるクライアントの作成

クライアントに @RegisterRestClient アノテーションを付け、クライアントを @RestClient で注入する代わりに、プログラムで REST クライアントを作成することもできます。これは RestClientBuilder で行います。

このアプローチでは、クライアントインターフェイスは次のようになります。

package org.acme.rest.client;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;

@Path("/extensions")
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);
}

そして、次のようなサービス:

package org.acme.rest.client;

import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.net.URI;
import java.util.Set;

@Path("/extension")
public class ExtensionsResource {

    private final ExtensionsService extensionsService;

    public ExtensionsResource() {
        extensionsService = QuarkusRestClientBuilder.newBuilder()
            .baseUri(URI.create("https://stage.code.quarkus.io/api"))
            .build(ExtensionsService.class);
    }

    @GET
    @Path("/id/{id}")
    public Set<Extension> id(String id) {
        return extensionsService.getById(id);
    }
}

The QuarkusRestClientBuilder interface is a Quarkus-specific API to programmatically create clients with additional configuration options. Otherwise, you can also use the RestClientBuilder interface from the Microprofile API:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.RestClientBuilder;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.net.URI;
import java.util.Set;

@Path("/extension")
public class ExtensionsResource {

    private final ExtensionsService extensionsService;

    public ExtensionsResource() {
        extensionsService = RestClientBuilder.newBuilder()
            .baseUri(URI.create("https://stage.code.quarkus.io/api"))
            .build(ExtensionsService.class);
    }

    // ...
}

カスタムHTTPオプションの使用

The REST Client internally uses the Vert.x HTTP Client to make the network connections. The REST Client extensions allows configuring some settings via properties, for example:

  • quarkus.rest-client.client-prefix.connect-timeout で、接続タイムアウトをミリ秒単位で設定します。

  • `quarkus.rest-client.client-prefix.max-redirects`でリダイレクトの回数を制限できます。

しかし、Vert.x HTTP Client 内には、接続を設定するための多くのオプションがあります。 このリンク のVert.x HTTP Client Options API ですべてのオプションを参照してください。

To fully customize the Vert.x HTTP Client instance that the REST Client is internally using, you can provide your custom HTTP Client Options instance via CDI or when programmatically creating your client.

ここでは、CDIを使用してHTTPクライアントオプションを提供する方法について、例を挙げて説明します:

package org.acme.rest.client;

import jakarta.enterprise.inject.Produces;
import jakarta.ws.rs.ext.ContextResolver;

import io.vertx.core.http.HttpClientOptions;
import io.quarkus.arc.Unremovable;

@Provider
public class CustomHttpClientOptions implements ContextResolver<HttpClientOptions> {

    @Override
    public HttpClientOptions getContext(Class<?> aClass) {
        HttpClientOptions options = new HttpClientOptions();
        // ...
        return options;
    }
}

これで、すべてのRESTクライアントが、あなたのカスタムHTTPクライアントオプションを使用するようになります。

もう一つの方法は、コードを用いてでクライアントを作成する際に、カスタムHTTPクライアントオプションを提供することです。

package org.acme.rest.client;

import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.net.URI;
import java.util.Set;

import io.vertx.core.http.HttpClientOptions;

@Path("/extension")
public class ExtensionsResource {

    private final ExtensionsService extensionsService;

    public ExtensionsResource() {
        HttpClientOptions options = new HttpClientOptions();
        // ...

        extensionsService = QuarkusRestClientBuilder.newBuilder()
            .baseUri(URI.create("https://stage.code.quarkus.io/api"))
            .httpClientOptions(options) (1)
            .build(ExtensionsService.class);
    }

    // ...
}
1 クライアントはCDI経由で提供されたHTTPクライアントオプションよりも、登録されたHTTPクライアントオプションを使用します。

リダイレクト

HTTPサーバーは、ステータスコードが "3 "で始まるレスポンスと、リダイレクト先のURLを保持するHTTPヘッダー "Location" を送信することで、レスポンスを別の場所にリダイレクトできます。RESTクライアントがHTTPサーバーからリダイレクトレスポンスを受信しても、新しい場所への再リクエストを自動的に実行することはありません。"follow-redirects"プロパティを追加することで、REST Clientで自動リダイレクトを有効にすることができます:

  • quarkus.rest-client.follow-redirects を有効にすると、すべてのRESTクライアントに対してリダイレクトが有効になります。

  • quarkus.rest-client.<client-prefix>.follow-redirects を有効にすると、特定のRESTクライアントに対するリダイレクトを有効にすることができます。

このプロパティがtrueの場合、REST Clientは、HTTPサーバーからリダイレクトレスポンスを受信すると、新しいリクエストを実行します。

さらに、プロパティ "max-redirects" を使って、リダイレクトの回数を制限することができます。

One important note is that according to the RFC2616 specs, by default the redirection will only happen for GET or HEAD methods. However, in REST Client, you can provide your custom redirect handler to enable redirection on POST or PUT methods, or to follow a more complex logic, via either using the @ClientRedirectHandler annotation, CDI or programmatically when creating your client.

Let’s see an example about how to register your own custom redirect handler using the @ClientRedirectHandler annotation:

import jakarta.ws.rs.core.Response;

import io.quarkus.rest.client.reactive.ClientRedirectHandler;

@RegisterRestClient(configKey="extensions-api")
public interface ExtensionsService {
    @ClientRedirectHandler
    static URI alwaysRedirect(Response response) {
        if (Response.Status.Family.familyOf(response.getStatus()) == Response.Status.Family.REDIRECTION) {
            return response.getLocation();
        }

        return null;
    }
}

The "alwaysRedirect" redirect handler will only be used by the specified REST Client which in this example is the "ExtensionsService" client.

Alternatively, you can also provide a custom redirect handler for all your REST Clients via CDI:

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;

import org.jboss.resteasy.reactive.client.handlers.RedirectHandler;

@Provider
public class AlwaysRedirectHandler implements ContextResolver<RedirectHandler> {

    @Override
    public RedirectHandler getContext(Class<?> aClass) {
        return response -> {
            if (Response.Status.Family.familyOf(response.getStatus()) == Response.Status.Family.REDIRECTION) {
                return response.getLocation();
            }
            // no redirect
            return null;
        };
    }
}

Now, all the REST Clients will be using your custom redirect handler.

Another approach is to provide it programmatically when creating the client:

@Path("/extension")
public class ExtensionsResource {

    private final ExtensionsService extensionsService;

    public ExtensionsResource() {
        extensionsService = QuarkusRestClientBuilder.newBuilder()
            .baseUri(URI.create("https://stage.code.quarkus.io/api"))
            .register(AlwaysRedirectHandler.class) (1)
            .build(ExtensionsService.class);
    }

    // ...
}
1 the client will use the registered redirect handler over the redirect handler provided via CDI if any.

テストの更新

次に、エンドポイントに加えられた変更を反映させるために、機能テストを更新する必要があります。src/test/java/org/acme/rest/client/ExtensionsResourceTest.java ファイルを編集し、テストの内容を以下のように変更します。

package org.acme.rest.client;

import io.quarkus.test.junit.QuarkusTest;

import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.greaterThan;

@QuarkusTest
public class ExtensionsResourceTest {

    @Test
    public void testExtensionsIdEndpoint() {
        given()
            .when().get("/extension/id/io.quarkus:quarkus-rest-client")
            .then()
            .statusCode(200)
            .body("$.size()", is(1),
                "[0].id", is("io.quarkus:quarkus-rest-client"),
                "[0].name", is("REST Client"),
                "[0].keywords.size()", greaterThan(1),
                "[0].keywords", hasItem("rest-client"));
    }
}

上記のコードでは、 REST Assuredjson-path 機能を使用しています。

非同期のサポート

To get the full power of the reactive nature of the client, you can use the non-blocking flavor of REST Client extension, which comes with support for CompletionStage and Uni. Let’s see it in action by adding a getByIdAsync method in our ExtensionsService REST interface. The code should look like:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;
import java.util.concurrent.CompletionStage;

@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);

    @GET
    CompletionStage<Set<Extension>> getByIdAsync(@QueryParam("id") String id);
}

src/main/java/org/acme/rest/client/ExtensionsResource.java ファイルを開き、以下の内容で更新します:

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Set;
import java.util.concurrent.CompletionStage;

@Path("/extension")
public class ExtensionsResource {

    @RestClient
    ExtensionsService extensionsService;


    @GET
    @Path("/id/{id}")
    public Set<Extension> id(String id) {
        return extensionsService.getById(id);
    }

    @GET
    @Path("/id-async/{id}")
    public CompletionStage<Set<Extension>> idAsync(String id) {
        return extensionsService.getByIdAsync(id);
    }
}

Please note that since the invocation is now non-blocking, the idAsync method will be invoked on the event loop, i.e. will not get offloaded to a worker pool thread and thus reducing hardware resource utilization. See Quarkus REST execution model for more details.

非同期メソッドをテストするには、ExtensionsResourceTest に以下のテストメソッドを追加します。

@Test
public void testExtensionIdAsyncEndpoint() {
    given()
        .when().get("/extension/id-async/io.quarkus:quarkus-rest-client")
        .then()
        .statusCode(200)
        .body("$.size()", is(1),
            "[0].id", is("io.quarkus:quarkus-rest-client"),
            "[0].name", is("REST Client"),
            "[0].keywords.size()", greaterThan(1),
            "[0].keywords", hasItem("rest-client"));
}

Uni 版は、非常に似ています。

package org.acme.rest.client;

import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;

@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {

    // ...

    @GET
    Uni<Set<Extension>> getByIdAsUni(@QueryParam("id") String id);
}

ExtensionsResource は以下ようになります。

package org.acme.rest.client;

import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Set;

@Path("/extension")
public class ExtensionsResource {

    @RestClient
    ExtensionsService extensionsService;


    // ...

    @GET
    @Path("/id-uni/{id}")
    public Uni<Set<Extension>> idUni(String id) {
        return extensionsService.getByIdAsUni(id);
    }
}
Mutiny

前のスニペットでは、Mutinyのリアクティブ型を使用していますが、もし慣れていない場合は、まず Mutiny - 直感的なリアクティブプログラミングライブラリ を読んでください。

Uni を返すとき、すべての サブスクリプション は 、リモートサービスを呼び出します。つまり、 Uni で再購読してリクエストを再送信するか、以下のように retry を使用することができます。

@RestClient ExtensionsService extensionsService;

// ...

extensionsService.getByIdAsUni(id)
    .onFailure().retry().atMost(10);

CompletionStage を使用する場合は、再試行のためにサービスのメソッドを呼び出す必要があります。この違いは、 Mutiny とそのサブスクリプションプロトコルの lazy (遅延) 性の側面から来ています。これについての詳細は Mutiny の ドキュメント を参照してください。

サーバー送信イベント (SSE) のサポート

Consuming SSE events is possible simply by declaring the result type as a io.smallrye.mutiny.Multi.

The simplest example is:

package org.acme.rest.client;

import io.smallrye.mutiny.Multi;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/sse")
@RegisterRestClient(configKey = "some-api")
public interface SseClient {
     @GET
     @Produces(MediaType.SERVER_SENT_EVENTS)
     Multi<String> get();
}

All the IO involved in streaming the SSE results is done in a non-blocking manner.

Results are not limited to strings - for example when the server returns JSON payload for each event, Quarkus automatically deserializes it into the generic type used in the Multi.

Users can also access the entire SSE event by using the org.jboss.resteasy.reactive.client.SseEvent type.

A simple example where the event payloads are Long values is the following:

package org.acme.rest.client;

import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.client.SseEvent;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@Path("/sse")
@RegisterRestClient(configKey = "some-api")
public interface SseClient {
     @GET
     @Produces(MediaType.SERVER_SENT_EVENTS)
     Multi<SseEvent<Long>> get();
}

Filtering out events

On occasion, the stream of SSE events may contain some events that should not be returned by the client - an example of this is having the server send heartbeat events in order to keep the underlying TCP connection open. The REST Client supports filtering out such events by providing the @org.jboss.resteasy.reactive.client.SseEventFilter.

Here is an example of filtering out heartbeat events:

package org.acme.rest.client;

import io.smallrye.mutiny.Uni;
import java.util.function.Predicate;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.client.SseEvent;
import org.jboss.resteasy.reactive.client.SseEventFilter;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@Path("/sse")
@RegisterRestClient(configKey = "some-api")
public interface SseClient {

     @GET
     @Produces(MediaType.SERVER_SENT_EVENTS)
     @SseEventFilter(HeartbeatFilter.class)
     Multi<SseEvent<Long>> get();


     class HeartbeatFilter implements Predicate<SseEvent<String>> {

        @Override
        public boolean test(SseEvent<String> event) {
            return !"heartbeat".equals(event.id());
        }
     }
}

カスタムヘッダーのサポート

REST 呼び出しのカスタムヘッダーを指定する方法はいくつかあります。

  • @RegisterClientHeaders アノテーションを用いて ClientHeadersFactory または ReactiveClientHeadersFactory を登録する。

  • by programmatically registering a ClientHeadersFactory or a ReactiveClientHeadersFactory with the QuarkusRestClientBuilder.clientHeadersFactory(factory) method

  • @ClientHeaderParam でヘッダーの値を指定する

  • @HeaderParam でヘッダーの値を指定する

以下のコードは、これらの各手法の使用方法を示しています。

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;
import io.quarkus.rest.client.reactive.NotBody;

@Path("/extensions")
@RegisterRestClient
@RegisterClientHeaders(RequestUUIDHeaderFactory.class) (1)
@ClientHeaderParam(name = "my-header", value = "constant-header-value") (2)
@ClientHeaderParam(name = "computed-header", value = "{org.acme.rest.client.Util.computeHeader}") (3)
public interface ExtensionsService {

    @GET
    @ClientHeaderParam(name = "header-from-properties", value = "${header.value}") (4)
    @ClientHeaderParam(name = "header-from-method-param", value = "Bearer {token}") (5)
    Set<Extension> getById(@QueryParam("id") String id, @HeaderParam("jaxrs-style-header") String headerValue, @NotBody String token); (6)
}
1 クラスごとに存在できる ClientHeadersFactory は 1 つだけです。これを使用すると、カスタムヘッダーを追加できるだけでなく、既存のヘッダーを変換することもできます。ファクトリーの例については、以下の RequestUUIDHeaderFactory クラスを参照してください。
2 @ClientHeaderParam は、クライアントインターフェイスとメソッドで使用できます。一定のヘッダー値を指定できます。
3 …​ and a name of a method that should compute the value of the header. It can either be a static method or a default method in this interface. The method can take either no parameters, a single String parameter or a single io.quarkus.rest.client.reactive.ComputedParamContext parameter (which is very useful for code that needs to compute headers based on method parameters and naturally complements @io.quarkus.rest.client.reactive.NotBody).
4 また、アプリケーションの設定値も使用します。
5 …​ or even any mixture of verbatim text, method parameters (referenced by name), a configuration value (as mentioned previously) and method invocations (as mentioned before)
6 …​ or as a normal Jakarta REST @HeaderParam annotated argument

Kotlin を使用するときにデフォルトのメソッドを利用する場合は、Java のデフォルトのインターフェイス機能を使用するように Kotlin コンパイラーを設定する必要があります。詳細は、こちら を参照してください。

ClientHeadersFactory は次のようになります。

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.UUID;

@ApplicationScoped
public class RequestUUIDHeaderFactory implements ClientHeadersFactory {

    @Override
    public MultivaluedMap<String, String> update(MultivaluedMap<String, String> incomingHeaders, MultivaluedMap<String, String> clientOutgoingHeaders) {
        MultivaluedMap<String, String> result = new MultivaluedHashMap<>();
        result.add("X-request-uuid", UUID.randomUUID().toString());
        return result;
    }
}

上の例のように、 @Singleton, @ApplicationScoped などのスコープを定義するアノテーションを付けることで、 ClientHeadersFactory の実装をCDI Beanにすることができます。

${header.value} に値を指定するには、application.properties に以下のように記述するだけです。

header.value=value of the header

また、ブロッキング操作を実行できる ClientHeadersFactory のリアクティブフレーバーがあります。例えば:

package org.acme.rest.client;

import io.smallrye.mutiny.Uni;

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.UUID;

@ApplicationScoped
public class GetTokenReactiveClientHeadersFactory extends ReactiveClientHeadersFactory {

    @Inject
    Service service;

    @Override
    public Uni<MultivaluedMap<String, String>> getHeaders(
            MultivaluedMap<String, String> incomingHeaders,
            MultivaluedMap<String, String> clientOutgoingHeaders) {
        return Uni.createFrom().item(() -> {
            MultivaluedHashMap<String, String> newHeaders = new MultivaluedHashMap<>();
            // perform blocking call
            newHeaders.add(HEADER_NAME, service.getToken());
            return newHeaders;
        });
    }
}

When using HTTP Basic Auth, the @io.quarkus.rest.client.reactive.ClientBasicAuth annotation provides a much simpler way of configuring the necessary Authorization header.

A very simple example is:

@ClientBasicAuth(username = "${service.username}", password = "${service.password}")
public interface SomeClient {

}

where service.username and service.password are configuration properties that must be set at runtime to the username and password that allow access to the service being called.

デフォルトのヘッダーファクトリー

@RegisterClientHeaders アノテーションは、カスタムファクトリーを指定せずに使用することもできます。その場合、DefaultClientHeadersFactoryImpl ファクトリーが使用されます。REST リソースから REST クライアント呼び出しを行う場合、このファクトリーは、org.eclipse.microprofile.rest.client.propagateHeaders 設定プロパティーにリストされているすべてのヘッダーをリソースリクエストからクライアントリクエストに伝播させます。個々のヘッダー名はコンマで区切られます。

@Path("/extensions")
@RegisterRestClient
@RegisterClientHeaders
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);

    @GET
    CompletionStage<Set<Extension>> getByIdAsync(@QueryParam("id") String id);
}
org.eclipse.microprofile.rest.client.propagateHeaders=Authorization,Proxy-Authorization

リクエストのカスタマイズ

The REST Client supports further customization of the final request to be sent to the server via filters. The filters must implement either the interface ClientRequestFilter or ResteasyReactiveClientRequestFilter.

A simple example of customizing the request would be to add a custom header:

@Provider
public class TestClientRequestFilter implements ClientRequestFilter {

    @Override
    public void filter(ClientRequestContext requestContext) {
        requestContext.getHeaders().add("my_header", "value");
    }
}

Next, you can register your filter using the @RegisterProvider annotation:

@Path("/extensions")
@RegisterProvider(TestClientRequestFilter.class)
public interface ExtensionsService {

    // ...
}

Or programmatically using the .register() method:

QuarkusRestClientBuilder.newBuilder()
    .register(TestClientRequestFilter.class)
    .build(ExtensionsService.class)

jakarta.ws.rs.ext.Providers インスタンスをフィルターに注入する

jakarta.ws.rs.ext.Providers は、現在のクライアントのプロバイダー・インスタンスを検索する必要がある場合に便利です。

We can get the Providers instance in our filters from the request context as follows:

@Provider
public class TestClientRequestFilter implements ClientRequestFilter {

    @Override
    public void filter(ClientRequestContext requestContext) {
        Providers providers = ((ResteasyReactiveClientRequestContext) requestContext).getProviders();
        // ...
    }
}

Alternatively, you can implement the ResteasyReactiveClientRequestFilter interface instead of the ClientRequestFilter interface that will directly provide the ResteasyReactiveClientRequestContext context:

@Provider
public class TestClientRequestFilter implements ResteasyReactiveClientRequestFilter {

    @Override
    public void filter(ResteasyReactiveClientRequestFilter requestContext) {
        Providers providers = requestContext.getProviders();
        // ...
    }
}

Customizing the ObjectMapper in REST Client Jackson

The REST Client supports adding a custom ObjectMapper to be used only the Client using the annotation @ClientObjectMapper.

A simple example is to provide a custom ObjectMapper to the REST Client Jackson extension by doing:

@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);

    @ClientObjectMapper (1)
    static ObjectMapper objectMapper(ObjectMapper defaultObjectMapper) { (2)
        return defaultObjectMapper.copy() (3)
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .disable(DeserializationFeature.UNWRAP_ROOT_VALUE);
    }
}
1 The method must be annotated with @ClientObjectMapper.
2 It’s must be a static method. Also, the parameter defaultObjectMapper will be resolved via CDI. If not found, it will throw an exception at runtime.
3 In this example, we’re creating a copy of the default object mapper. You should NEVER modify the default object mapper, but create a copy instead.

例外処理

MicroProfile REST クライアント仕様では、HTTP 応答を例外に変換することを目的とした org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper が導入されています。

上記の ExtensionsService にこのような ResponseExceptionMapper を実装する簡単な例は、次のとおりです。

public class MyResponseExceptionMapper implements ResponseExceptionMapper<RuntimeException> {

    @Override
    public RuntimeException toThrowable(Response response) {
        if (response.getStatus() == 500) {
            throw new RuntimeException("The remote service responded with HTTP 500");
        }
        return null;
    }
}

ResponseExceptionMapper は、ResponseExceptionMapper 実装が呼び出される優先度を決定するために使用される getPriority メソッドも定義します (getPriority の値が小さい実装が最初に呼び出されます)。toThrowable が例外を返す場合、その例外が出力されます。null が返された場合、チェーン内の ResponseExceptionMapper の次の実装が呼び出されます (存在する場合)。

上記のクラスは、REST クライアントによって自動的に使用されることはありません。アプリケーションのすべての REST クライアントで使用できるようにするには、クラスに @Provider アノテーションを付ける必要があります (quarkus.rest-client-reactive.provider-autodiscoveryfalse に設定されていない場合)。または、例外処理クラスを特定の REST クライアントインターフェイスにのみ適用する必要がある場合は、インターフェイスに @RegisterProvider(MyResponseExceptionMapper.class) でアノテーションを付けるか、適切な quarkus.rest-client 設定グループの providers プロパティー設定を使用して登録できます。

@ClientExceptionMapper を使用する

400 以上の HTTP 応答コードを変換する簡単な方法は、@ClientExceptionMapper アノテーションを使用することです。

上記で定義された ExtensionsService REST クライアントインターフェイスの場合、@ClientExceptionMapper の使用例は次のようになります。

@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {

    @GET
    Set<Extension> getById(@QueryParam("id") String id);

    @GET
    CompletionStage<Set<Extension>> getByIdAsync(@QueryParam("id") String id);

    @ClientExceptionMapper
    static RuntimeException toException(Response response) {
        if (response.getStatus() == 500) {
            return new RuntimeException("The remote service responded with HTTP 500");
        }
        return null;
    }
}

当然、この処理は REST クライアントごとに行われます。@ClientExceptionMapper は、priority 属性が設定されておらず、すべてのハンドラーを呼び出す通常のルールが適用される場合、デフォルトの優先度を使用します。

@ClientExceptionMapper でアノテーションされたメソッドは、 java.lang.reflect.Method パラメータを取れます。これは、例外対応のコードは、呼び出されて例外を発生したREST Client メソッドを知る場合に便利です。

例外マッパーでの@Blockingアノテーションの使用

In cases that warrant using InputStream as the return type of REST Client method (such as when large amounts of data need to be read):

@Path("/echo")
@RegisterRestClient
public interface EchoClient {

    @GET
    InputStream get();
}

This will work as expected, but if you try to read this InputStream object in a custom exception mapper, you will receive a BlockingNotAllowedException exception. This is because ResponseExceptionMapper classes are run on the Event Loop thread executor by default - which does not allow to perform IO operations.

To make your exception mapper blocking, you can annotate the exception mapper with the @Blocking annotation:

@Provider
@Blocking (1)
public class MyResponseExceptionMapper implements ResponseExceptionMapper<RuntimeException> {

    @Override
    public RuntimeException toThrowable(Response response) {
        if (response.getStatus() == 500) {
            response.readEntity(String.class); (2)
            return new RuntimeException("The remote service responded with HTTP 500");
        }
        return null;
    }
}
1 @Blocking アノテーションにより、MyResponseExceptionMapper例外マッパーはワーカースレッドプールで実行されることになります。
2 ワーカースレッドプールでマッパーを実行しているため、エンティティの読み取りが許可されるようになりました。

なお、@ClientExceptionMapperを使用する場合は、 @Blocking アノテーションを使用することも可能です:

@Path("/echo")
@RegisterRestClient
public interface EchoClient {

    @GET
    InputStream get();

    @ClientExceptionMapper
    @Blocking
    static RuntimeException toException(Response response) {
        if (response.getStatus() == 500) {
            response.readEntity(String.class);
            return new RuntimeException("The remote service responded with HTTP 500");
        }
        return null;
    }
}

マルチパートフォームのサポート

REST Client support multipart messages.

マルチパートメッセージの送信

REST Client allows sending data as multipart forms. This way you can for example send files efficiently.

データをマルチパートフォームとして送信するには、通常の @RestForm (または @FormParam ) アノテーションを使用します:

    @POST
    @Path("/binary")
    String sendMultipart(@RestForm File file, @RestForm String otherField);

File , Path , byte[] , Buffer として指定されたパラメータは、ファイルとして送信され、デフォルトで application/octet-stream MIME タイプになります。その他の @RestForm パラメータ型のデフォルトは、 text/plain MIMEタイプです。これらのデフォルトを上書きするには、 @PartType アノテーションを使用します。

当然ながら、これらのパラメータを含むクラスにまとめられます:

    public static class Parameters {
        @RestForm
        File file;

        @RestForm
        String otherField;
    }

    @POST
    @Path("/binary")
    String sendMultipart(Parameters parameters);

File , Path , byte[] , Buffer , および @PartType のアノテーションが付いた @RestForm パラメータは、 @Consumes が存在しない場合、そのメソッドが @Consumes(MediaType.MULTIPART_FORM_DATA) を自動的に意味します。

マルチパートではない @RestForm パラメータがある場合、 その時は @Consumes(MediaType.APPLICATION_FORM_URLENCODED) になります。

There are a few modes in which the form data can be encoded. By default, REST Client uses RFC1738. You can override it by specifying the mode either on the client level, by setting io.quarkus.rest.client.multipart-post-encoder-mode RestBuilder property to the selected value of HttpPostRequestEncoder.EncoderMode or by specifying quarkus.rest-client.multipart-post-encoder-mode in your application.properties. Please note that the latter works only for clients created with the @RegisterRestClient annotation. All the available modes are described in the Netty documentation

また、 @PartType アノテーションを指定することで、JSON のマルチパートも送信できます。

    public static class Person {
        public String firstName;
        public String lastName;
    }

    @POST
    @Path("/json")
    String sendMultipart(@RestForm @PartType(MediaType.APPLICATION_JSON) Person person);

Programmatically creating the Multipart form

In cases where the multipart content needs to be built up programmatically, the REST Client provides ClientMultipartForm which can be used in the REST Client like so:

public interface MultipartService {

  @POST
  @Path("/multipart")
  @Consumes(MediaType.MULTIPART_FORM_DATA)
  @Produces(MediaType.APPLICATION_JSON)
  Map<String, String> multipart(ClientMultipartForm dataParts);
}

More information about this class and supported methods can be found on the javadoc of ClientMultipartForm.

Converting a received multipart object into a client request

A good example of creating ClientMultipartForm is one where it is created from the server’s MultipartFormDataInput (which represents a multipart request received by Quarkus REST) - the purpose being to propagate the request downstream while allowing for arbitrary modifications:

public ClientMultipartForm buildClientMultipartForm(MultipartFormDataInput inputForm) (1)
    throws IOException {
  ClientMultipartForm multiPartForm = ClientMultipartForm.create(); (2)
  for (Entry<String, Collection<FormValue>> attribute : inputForm.getValues().entrySet()) {
    for (FormValue fv : attribute.getValue()) {
      if (fv.isFileItem()) {
        final FileItem fi = fv.getFileItem();
        String mediaType = Objects.toString(fv.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE),
            MediaType.APPLICATION_OCTET_STREAM);
        if (fi.isInMemory()) {
          multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(),
              Buffer.buffer(IOUtils.toByteArray(fi.getInputStream())), mediaType); (3)
        } else {
          multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(),
              fi.getFile().toString(), mediaType); (4)
        }
      } else {
        multiPartForm.attribute(attribute.getKey(), fv.getValue(), fv.getFileName()); (5)
      }
    }
  }
  return multiPartForm;
}
1 MultipartFormDataInput is a Quarkus REST (Server) type representing a received multipart request.
2 A ClientMultipartForm is created.
3 FileItem attribute is created for the request attribute that represented an in memory file attribute
4 FileItem attribute is created for the request attribute that represented a file attribute saved on the file system
5 Non-file attributes added directly to ClientMultipartForm if not FileItem.

In a similar fashion if the received server multipart request is known and looks something like:

public class Request { (1)

  @RestForm("files")
  @PartType(MediaType.APPLICATION_OCTET_STREAM)
  List<FileUpload> files;

  @RestForm("jsonPayload")
  @PartType(MediaType.TEXT_PLAIN)
  String jsonPayload;
}

the ClientMultipartForm can be created easily as follows:

public ClientMultipartForm buildClientMultipartForm(Request request) { (1)
  ClientMultipartForm multiPartForm = ClientMultipartForm.create();
  multiPartForm.attribute("jsonPayload", request.getJsonPayload(), "jsonPayload"); (2)
  request.getFiles().forEach(fu -> {
    multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); (3)
  });
  return multiPartForm;
}
1 Request representing the request the server parts accepts
2 A jsonPayload attribute is added directly to ClientMultipartForm
3 A binaryFileUpload is created from the request’s FileUpload (which is a Quarkus REST (Server) type used to represent a binary file upload)

When sending multipart data that uses the same name, problems can arise if the client and server do not use the same multipart encoder mode. By default, the REST Client uses RFC1738, but depending on the situation, clients may need to be configured with HTML5 or RFC3986 mode.

This configuration can be achieved via the quarkus.rest-client.multipart-post-encoder-mode property.

マルチパートメッセージの受信

REST Client also supports receiving multipart messages. As with sending, to parse a multipart response, you need to create a class that describes the response data, e.g.

public class FormDto {
    @RestForm (1)
    @PartType(MediaType.APPLICATION_OCTET_STREAM)
    public File file;

    @FormParam("otherField") (2)
    @PartType(MediaType.TEXT_PLAIN)
    public String textProperty;
}
1 省略形の @RestForm アノテーションを使用して、マルチパートフォームの一部としてフィールドを作成します
2 標準の @FormParam も使用できます。マルチパートパートの名前を上書きできます。

次に、呼び出しに対応するインターフェイスメソッドを作成し、FormDto を返すようにします。

    @GET
    @Produces(MediaType.MULTIPART_FORM_DATA)
    @Path("/get-file")
    FormDto data receiveMultipart();

現時点では、マルチパート応答のサポートには次の制限があります。

  • マルチパート応答で送信されたファイルは、FilePath、および FileDownload にのみ解析できます。

  • 応答タイプの各フィールドには @PartType アノテーションを付ける必要があります - このアノテーションのないフィールドは無視されます

REST Client needs to know the classes used as multipart return types upfront. If you have an interface method that produces multipart/form-data, the return type will be discovered automatically. However, if you intend to use the ClientBuilder API to parse a response as multipart, you need to annotate your DTO class with @MultipartForm.

ダウンロードしたファイルは自動的に削除されず、多くのディスク領域を占有する可能性があります。作業が終わったら、ファイルを削除することを検討してください。

Multipart mixed / OData usage

It is not uncommon that an application has to interact with enterprise systems (like CRM systems) using a special protocol called OData. This protocol essentially uses a custom HTTP Content-Type which needs some glue code to work with the REST Client (creating the body is entirely up to the application - the REST Client can’t do much to help).

例は次のようになります:

@Path("/crm")
@RegisterRestClient
public interface CRMService {

    @POST
    @ClientHeaderParam(name = "Content-Type", value = "{calculateContentType}")  (1)
    String performBatch(@HeaderParam("Authorization") String accessToken, @NotBody String batchId, String body); (2)

    default String calculateContentType(ComputedParamContext context) {
        return "multipart/mixed;boundary=batch_" + context.methodParameters().get(1).value(); (3)
    }
}

The code uses the following pieces:

1 @ClientHeaderParam(name = "Content-Type", value = "{calculateContentType}") which ensures that the Content-Type header is created by calling the interface’s calculateContentType default method.
2 The aforementioned parameter needs to be annotated with @NotBody because it is only used to aid the construction of HTTP headers.
3 context.methodParameters().get(1).value() which allows the calculateContentType method to obtain the proper method parameter passed to the REST Client method.

As previously mentioned, the body parameter needs to be properly crafted by the application code to conform to the service’s requirements.

圧縮メッセージの受信

REST Client は、GZIP を使用した圧縮メッセージの受信もサポートしています。HTTP 圧縮のサポートを有効にするには、プロパティ quarkus.http.enable-compression=true を追加します。こ の機能が有効で、サーバがヘッダ Content-Encoding: gzip を含む応答を返すと、REST Client は自動的にコンテンツをデコードし、メッセージの処理を続行します。

プロキシーサポート

REST Client はプロキシ経由でのリクエスト送信をサポートしています。 JVMの設定を尊重しますが、両方を指定することもできます:

  • グローバルクライアントプロキシー設定、 quarkus.rest-client.proxy-addressquarkus.rest-client.proxy-userquarkus.rest-client.proxy-passwordquarkus.rest-client.non-proxy-hosts

  • quarkus.rest-client.<my-client>.proxy-address などのクライアントごとのプロキシー設定です。これらは、CDI で挿入されたクライアント、つまり @RegisterRestClient で作成されたクライアントに対してのみ適用されます。

proxy-address がクライアントレベルで設定されている場合、クライアントは特定のプロキシー設定を使用します。プロキシー設定は、グローバル設定または JVM プロパティーから伝播されません。

クライアントに proxy-address が設定されていないが、グローバルレベルで設定されている場合、クライアントはグローバル設定を使用します。それ以外の場合、クライアントは JVM 設定を使用します。

プロキシーを設定するための設定例:

# global proxy configuration is used for all clients
quarkus.rest-client.proxy-address=localhost:8182
quarkus.rest-client.proxy-user=<proxy user name>
quarkus.rest-client.proxy-password=<proxy password>
quarkus.rest-client.non-proxy-hosts=example.com

# per-client configuration overrides the global settings for a specific client
quarkus.rest-client.my-client.proxy-address=localhost:8183
quarkus.rest-client.my-client.proxy-user=<proxy user name>
quarkus.rest-client.my-client.proxy-password=<proxy password>
quarkus.rest-client.my-client.url=...
MicroProfile REST クライアント仕様では、プロキシー資格情報の設定は許可されていません。プログラムでプロキシーユーザーとプロキシーパスワードを指定するには、RestClientBuilderRestClientBuilderImpl にキャストする必要があります。

アプリケーションのパッケージ化と実行

アプリケーションを以下のように実行します。

コマンドラインインタフェース
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

このエクステンションに関する基本的な情報を含む JSON オブジェクトが表示されるはずです。

アプリケーションは以下の方法でパッケージ化されます。

コマンドラインインタフェース
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

そして、 java -jar target/quarkus-app/quarkus-run.jar で実行します。

以下のように、ネイティブ実行可能ファイルを生成することもできます。

コマンドラインインタフェース
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native

トラフィックの記録

REST Client は、送信したリクエストと受信したレスポンスをログに記録できます。 ロギングを有効にするには、 application.propertiesquarkus.rest-client.logging.scope プロパティを追加し、次のように設定します:

  • リクエストと応答の内容をログに記録する request-response 、または

  • all は、基礎となるライブラリーの低レベルのロギングも有効にします。

HTTP メッセージには大きな本文が含まれる可能性があるため、ログに記録される本文の文字数を制限します。デフォルトの制限は 100 ですが、quarkus.rest-client.logging.body-limit を指定することで変更できます。

REST クライアントは、レベル DEBUG でトラフィックをロギングし、ロガー プロパティを変更しません。この機能を使用するには、ロガー設定を調整する必要があります。

ロギング設定の例:

quarkus.rest-client.logging.scope=request-response
quarkus.rest-client.logging.body-limit=50

quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG

REST Client はデフォルトの ClientLogger 実装を使用します。この実装は、カスタム実装に置き換えることができます。

QuarkusRestClientBuilder を使用してプログラムでクライアントを設定する場合、 ClientLoggerclientLogger メソッドで設定されます。

For declarative clients using @RegisterRestClient, simply providing a CDI bean that implements ClientLogger is enough for that logger to be used by said clients.

テストのためのクライアントのモッキング

@RestClient アノテーションが挿入されたクライアントを使用する場合は、テスト用に簡単にモックすることができます。Mockito の @InjectMock または QuarkusMock でそれを行うことができます。

このセクションでは、クライアントをモックに置き換える方法を示します。Quarkus でモッキングがどのように機能するかをより深く理解したい場合は、 Mocking CDI beans のブログ投稿を参照してください。

@QuarkusIntegrationTest を使用した場合、モッキングは動作しません。

次のクライアントがあると仮定します。

package io.quarkus.it.rest.client.main;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;


@Path("/")
@RegisterRestClient
public interface Client {
    @GET
    String get();
}

InjectMock でモックする

テスト用にクライアントをモックする最も簡単な方法は、Mockito と @InjectMock を使用することです。

まず、以下の依存関係をアプリケーションに追加します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-junit5-mockito")

次に、テストで @InjectMock を使用して、モックを作成して挿入できます。

package io.quarkus.it.rest.client.main;

import static org.mockito.Mockito.when;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;

@QuarkusTest
public class InjectMockTest {

    @InjectMock
    @RestClient
    Client mock;

    @BeforeEach
    public void setUp() {
        when(mock.get()).thenReturn("MockAnswer");
    }

    @Test
    void doTest() {
        // ...
    }
}

QuarkusMock でモックする

Mockito がニーズを満たさない場合は、QuarkusMock を使用してプログラムでモックを作成できます。例:

package io.quarkus.it.rest.client.main;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusMock;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class QuarkusMockTest {

    @BeforeEach
    public void setUp() {
        Client customMock = new Client() { (1)
            @Override
            public String get() {
                return "MockAnswer";
            }
        };
        QuarkusMock.installMockForType(customMock, Client.class, RestClient.LITERAL); (2)
    }
    @Test
    void doTest() {
        // ...
    }
}
1 ここでは、手動で作成したクライアントインターフェイスの実装を使用して、実際のクライアントを置き換えます
2 RestClient.LITERALinstallMockForType メソッドの最後の引数として渡される必要があることに注意してください

テスト時のモック HTTP サーバーの利用

テストを実行する際にモック HTTP サーバーを設定することは、一般的なテストパターンです。そのようなサーバーの例としては、 WiremockHoverfly があります。このセクションでは、ここまでで開発した ExtensionsService をテストするために Wiremock を活用する方法を示します。

最初に、 Wiremock をテストの依存関係として追加する必要があります。 Maven プロジェクトの場合は以下のようになります。

pom.xml
<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
    <version>${wiremock.version}</version> (1)
</dependency>
1 適切なWiremockバージョンを使用してください。利用可能なすべてのバージョンは、 こちら をご覧ください。
build.gradle
testImplementation("org.wiremock:wiremock:$wiremockVersion") (1)
1 適切なWiremockバージョンを使用してください。利用可能なすべてのバージョンは、 こちら をご覧ください。

Quarkusのテストを実行する前にサービスを開始する必要がある場合、Quarkusのテストでは、 @io.quarkus.test.common.QuarkusTestResource アノテーションを利用して、サービスを開始できる io.quarkus.test.common.QuarkusTestResourceLifecycleManager を指定し、Quarkusが使用する設定値を提供します。

@QuarkusTestResource の詳細については、 ドキュメントのこの部分を参照してください。

WiremockExtensions という名前で、 QuarkusTestResourceLifecycleManager の実装を以下のように作成します。

package org.acme.rest.client;

import java.util.Map;

import com.github.tomakehurst.wiremock.WireMockServer;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

import static com.github.tomakehurst.wiremock.client.WireMock.*; (1)

public class WireMockExtensions implements QuarkusTestResourceLifecycleManager {  (2)

    private WireMockServer wireMockServer;

    @Override
    public Map<String, String> start() {
        wireMockServer = new WireMockServer();
        wireMockServer.start(); (3)

        wireMockServer.stubFor(get(urlEqualTo("/extensions?id=io.quarkus:quarkus-rest-client"))   (4)
                .willReturn(aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withBody(
                            "[{" +
                            "\"id\": \"io.quarkus:quarkus-rest-client\"," +
                            "\"name\": \"REST Client\"" +
                            "}]"
                        )));

        wireMockServer.stubFor(get(urlMatching(".*")).atPriority(10).willReturn(aResponse().proxiedFrom("https://stage.code.quarkus.io/api")));   (5)

        return Map.of("quarkus.rest-client.\"org.acme.rest.client.ExtensionsService\".url", wireMockServer.baseUrl()); (6)
    }

    @Override
    public void stop() {
        if (null != wireMockServer) {
            wireMockServer.stop();  (7)
        }
    }
}
1 Wiremock パッケージのメソッドを静的にインポートすることで、テストが読みやすくなります。
2 start メソッドは、テストを実行する前に Quarkus によって呼び出され、テスト実行中に適用される設定プロパティーの Map を返します。
3 Wiremock を起動します。
4 /extensions?id=io.quarkus:quarkus-rest-client への呼び出しに対して特定のレスポンスを返すよう、Wiremock のスタブを設定します。
5 スタブ化されていないすべての HTTP 呼び出しは、実際のサービスを呼び出すことで処理されます。これはデモンストレーションを目的として行われているものであり、通常は実際のテストでは発生しません。
6 start メソッドはテストに適用される設定を返すので、 ExtensionsResource の実装で使用されるベースURLを制御する rest-client プロパティーを、Wiremock がリクエストの着信をリッスンするベースURLに設定します。
7 すべてのテストが終了したら、Wiremock を終了します。

ExtensionsResourceTest クラスには、以下のようなアノテーションが必要です。

@QuarkusTest
@QuarkusTestResource(WireMockExtensions.class)
public class ExtensionsResourceTest {

}

@QuarkusTestResourceExtensionsResourceTest だけでなく、すべてのテストに適用されます。

既知の制限

While the REST Client extension aims to be a drop-in replacement for the RESTEasy Client extension, there are some differences and limitations:

  • the default scope of the client for the new extension is @ApplicationScoped while the quarkus-resteasy-client defaults to @Dependent To change this behavior, set the quarkus.rest-client-reactive.scope property to the fully qualified scope name.

  • HostnameVerifier または SSLContext を設定することができません

  • ExecutorService の設定など、非ブロッキング実装には意味をなさないいくつかのことが機能しません

さらに詳しく

関連コンテンツ