Using the REST Client

本指南解释了如何使用 REST 客户端与 REST API 交互。REST 客户端与 Quarkus REST(之前的 RESTEasy Reactive)兼容的 REST 客户端实现。

This guide explains how to use the REST Client in order to interact with REST APIs. REST Client is the REST Client implementation compatible with Quarkus REST (formerly RESTEasy Reactive).

如果您的应用程序使用客户端并公开 REST 终端节点,请对服务器部分使用Quarkus REST

If your application uses a client and exposes REST endpoints, please use Quarkus REST for the server part.

Prerequisites

Unresolved directive in rest-client.adoc - include::{includes}/prerequisites.adoc[]

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

该解决方案位于`rest-client-quickstart` directory

The solution is located in the rest-client-quickstart directory.

Creating the Maven project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

First, we need a new project. Create a new project with the following command:

Unresolved directive in rest-client.adoc - include::{includes}/devtools/create-app.adoc[]

此命令生成具有 REST 端点的 Maven 项目,并导入:

This command generates the Maven project with a REST endpoint and imports:

  • the rest-jackson extension for the REST server support. Use rest instead if you do not wish to use Jackson;

  • the rest-client-jackson extension for the REST client support. Use rest-client instead if you do not wish to use Jackson

如果您已配置 Quarkus 项目,则可以通过在项目基本目录中运行以下命令将`rest-client-jackson`扩展添加到项目中:

If you already have your Quarkus project configured, you can add the rest-client-jackson extension to your project by running the following command in your project base directory:

Unresolved directive in rest-client.adoc - include::{includes}/devtools/extension-add.adoc[]

这会将以下内容添加到构建文件中:

This will add the following to your build file:

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

Setting up the model

在本指南中,我们将演示如何使用 stage.code.quarkus.io服务提供的 REST API 的一部分。我们的首要任务是设置我们将要使用的模型,以`Extension`POJO 的形式。

In this guide we will be demonstrating how to consume part of the REST API supplied by the stage.code.quarkus.io service. Our first order of business is to set up the model we will be using, in the form of a Extension POJO.

创建一个 src/main/java/org/acme/rest/client/Extension.java 文件,并设置以下内容:

Create a src/main/java/org/acme/rest/client/Extension.java file and set the following content:

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;

}

以上模型只是该服务所提供的字段的一个子集,但足以满足本指南的目的。

The model above is only a subset of the fields provided by the service, but it suffices for the purposes of this guide.

Create the interface

使用 REST 客户端与使用适当的 Jakarta REST 和 MicroProfile 注释创建接口一样简单。对于我们的案例,应该在`src/main/java/org/acme/rest/client/ExtensionsService.java`处创建接口,并具有以下内容:

Using the REST Client is as simple as creating an interface using the proper Jakarta REST and MicroProfile annotations. In our case the interface should be created at src/main/java/org/acme/rest/client/ExtensionsService.java and have the following content:

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 方法为我们的代码提供从 Code Quarkus API 根据 id 获取扩展的能力。客户端将处理所有网络连接和封送,使我们的代码不包含此类技术细节。

The getById method gives our code the ability to get an extension by id from the Code Quarkus API. The client will handle all the networking and marshalling leaving our code clean of such technical details.

上述代码中注释的目的如下:

The purpose of the annotations in the code above is the following:

  • @RegisterRestClient allows Quarkus to know that this interface is meant to be available for CDI injection as a REST Client

  • @Path, @GET and @QueryParam are the standard Jakarta REST annotations used to define how to access the service

当安装`quarkus-rest-client-jackson`扩展时,除非通过`@Produces`或`@Consumes`注释明确设置媒体类型,否则 Quarkus 将对大多数返回值默认使用`application/json`媒体类型。

When the quarkus-rest-client-jackson extension is installed, Quarkus will use the application/json media type by default for most return values, unless the media type is explicitly set via @Produces or @Consumes annotations.

如果你不依赖于 JSON 默认值,强烈建议使用 @Produces@Consumes 注释为端点添加注释,以精确定义预期 Content-Type。这将使你可以减少原生可执行文件中包含的 Jakarta REST 提供程序(可以看作转换器)的数量。

If you don’t rely on the JSON default, it is heavily recommended to annotate your endpoints with the @Produces and @Consumes annotations to define precisely the expected content-types. It will allow to narrow down the number of Jakarta REST providers (which can be seen as converters) included in the native executable.

上述 getById 方法为阻塞调用。 不应在事件循环中调用它。 Async Support 部分描述如何进行非阻塞调用。

The getById method above is a blocking call. It should not be invoked on the event loop. The Async Support section describes how to make non-blocking calls.

Query Parameters

指定查询参数的最简单方法是用 @QueryParam`或 `@RestQuery`注释客户端方法参数。 `@RestQuery 等同于 @QueryParam,但名称是可选的。此外,它还可以用于将查询参数作为 Map 传递,如果预先不知道参数,这样做很方便。

The easiest way to specify a query parameter is to annotate a client method parameter with the @QueryParam or the @RestQuery. The @RestQuery is equivalent of the @QueryParam, but with optional name. Additionally, it can be also used to pass query parameters as a Map, which is convenient if parameters are not known in advance.

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 Request query will include parameter with key name
2 Each Map entry represents exactly one query parameter
3 MultivaluedMap allows you to send array values

Using @ClientQueryParam

向请求添加查询参数的另一种方法是在 REST 客户端接口或接口的特定方法上使用 @io.quarkus.rest.client.reactive.ClientQueryParam。注释可以指定查询参数名称,而值可以是常量、配置文件属性,或者可以通过调用方法来确定。

Another way to add query parameters to a request is to use @io.quarkus.rest.client.reactive.ClientQueryParam on either the REST client interface or a specific method of the interface. The annotation can specify the query parameter name while the value can either be a constant, a configuration property or it can be determined by invoking a method.

以下示例显示了各种可能的用法:

The following example shows the various possible usages:

@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 By placing @ClientQueryParam on the interface, we ensure that my-param will be added to all requests of the client. Because we used the ${…​} syntax, the actual value of the parameter will be obtained using the my.property-value configuration property.
2 When getWithOtherParam is called, in addition to the my-param query parameter, some-other-param with the value of other will also be added.
3 when getFromMethod is called, in addition to the my-param query parameter, param-from-method with the value of test (because that’s what the withParam method returns when invoked with param-from-method) will also be added.

请注意,如果接口方法包含用 @QueryParam 注释的参数,则该参数将优先于任何 @ClientQueryParam 注释中指定的内容。

Note that if an interface method contains an argument annotated with @QueryParam, that argument will take priority over anything specified in any @ClientQueryParam annotation.

有关此注释的更多信息,请参阅 @ClientQueryParam 的 javadoc。

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

Form Parameters

可以使用 @RestForm (或 @FormParam)注释指定表单参数:

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

}

Using @ClientFormParam

也可以使用 @ClientFormParam 指定表单参数,类似于 @ClientQueryParam

Form parameters can also be specified using @ClientFormParam, similar to @ClientQueryParam:

@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。

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

Path Parameters

如果 GET 请求需要路径参数,则可以利用 @PathParam("parameter-name") 注释,而不是(或除了) @QueryParam。可以根据需要结合路径和查询参数,如下面的示例所示。

If the GET request requires path parameters you can leverage the @PathParam("parameter-name") annotation instead of (or in addition to) the @QueryParam. Path and query parameters can be combined, as required, as illustrated in the example below.

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

Sending large payloads

如果使用以下类型之一,REST 客户端能够在不将内容缓存在内存中发送任意的 HTTP body:

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

Create the configuration

要确定发起 REST 调用时的基本 URL,REST 客户端使用 application.properties 的配置。属性的名称需要遵循特定的约定,这在下面的代码中展示得最好:

In order to determine the base URL to which REST calls will be made, the REST Client uses configuration from application.properties. The name of the property needs to follow a certain convention which is best displayed in the following code:

# 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 must 匹配我们之前部分中创建的 ExtensionsService 界面中的完整限定名。

Note that org.acme.rest.client.ExtensionsService must match the fully qualified name of the ExtensionsService interface we created in the previous section.

为了方便配置,你可以使用 @RegisterRestClient configKey 属性,它允许使用与你的接口的完全限定名不同的配置根。

To facilitate the configuration, you can use the @RegisterRestClient configKey property that allows to use different configuration root than the fully qualified name of your interface.

@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

Disabling Hostname Verification

要为特定 REST 客户端禁用 SSL 主机名验证,请将以下属性添加到你的配置中:

To disable the SSL hostname verification for a specific REST client, add the following property to your configuration:

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

此设置不应在生产环境中使用,因为它会禁用 SSL 主机名验证。

This setting should not be used in production as it will disable the SSL hostname verification.

HTTP/2 Support

HTTP/2 在 REST 客户端中默认处于禁用状态。如果你想启用它,你可以设置:

HTTP/2 is disabled by default in REST Client. If you want to enable it, you can set:

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

或者,你可以启用应用层协议协商 (alpn) TLS 扩展,客户端将协商通过与服务器兼容的 HTTP 版本进行使用。默认情况下,它将首先尝试使用 HTTP/2,如果它未启用,将使用 HTTP/1.1。如果你想启用它,你可以设置:

Alternatively, you can enable the Application-Layer Protocol Negotiation (alpn) TLS extension and the client will negotiate which HTTP version to use over the ones compatible by the server. By default, it will try to use HTTP/2 first and if it’s not enabled, it will use HTTP/1.1. If you want to enable it, you can set:

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

Create the Jakarta REST resource

使用以下内容创建 src/main/java/org/acme/rest/client/ExtensionsResource.java 文件:

Create the src/main/java/org/acme/rest/client/ExtensionsResource.java file with the following content:

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

此列表中有两个有趣的部分:

There are two interesting parts in this listing:

1 the client stub is injected with the @RestClient annotation instead of the usual CDI @Inject

Programmatic client creation with QuarkusRestClientBuilder

与用 @RegisterRestClient 注释客户端以及用 @RestClient 注入客户端不同,您还可以以编程方式创建 REST 客户端。您可以用 QuarkusRestClientBuilder 来完成此操作。

Instead of annotating the client with @RegisterRestClient, and injecting a client with @RestClient, you can also create REST Client programmatically. You do that with the QuarkusRestClientBuilder.

使用该方式,客户端接口可以如下所示:

With this approach the client interface could look as follows:

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

该服务如下所示:

And the service as follows:

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

QuarkusRestClientBuilder 接口是 Quarkus 特有的 API,可以以编程方式创建具有其他配置选项的客户端。否则,您还可以使用 Microprofile API 中的 RestClientBuilder 接口:

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

    // ...
}

Use Custom HTTP Options

REST 客户端在内部使用 the Vert.x HTTP Client 来进行网络连接。REST 客户端扩展允许通过属性配置一些设置,例如:

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 to configure the connect timeout in milliseconds.

  • quarkus.rest-client.client-prefix.max-redirects to limit the number of redirects.

但是,Vert.x HTTP 客户端中还有更多选项可以配置连接。请参阅 this link 中 Vert.x HTTP 客户端选项 API 中的所有选项。

However, there are many more options within the Vert.x HTTP Client to configure the connections. See all the options in the Vert.x HTTP Client Options API in this link.

若要完全自定义 REST 客户端在内部使用的 Vert.x HTTP 客户端实例,您可以通过 CDI 在以编程方式创建客户端时提供自定义 HTTP 客户端选项实例。

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 客户端选项的示例:

Let’s see an example about how to provide the HTTP Client Options via CDI:

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 客户端选项。

Now, all the REST Clients will be using your custom HTTP Client Options.

另一种方法是在以编程方式创建客户端时提供自定义 HTTP 客户端选项:

Another approach is to provide the custom HTTP Client options when creating the client programmatically:

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 the client will use the registered HTTP Client options over the HTTP Client options provided via CDI if any.

Redirection

HTTP 服务器可以将响应重定向到其他位置,方法是发送一个状态代码以“3”开头的响应,以及一个包含要重定向到的 URL 的 HTTP 标头“Location”。当 REST Client 从 HTTP 服务器接收重定向响应时,它不会自动对新位置执行另一个请求。我们可以在 REST Client 中添加“follow-redirects”属性来启用自动重定向:

A HTTP server can redirect a response to another location by sending a response with a status code that starts with "3" and a HTTP header "Location" holding the URL to be redirected to. When the REST Client receives a redirection response from a HTTP server, it won’t automatically perform another request to the new location. We can enable the automatic redirection in REST Client by adding the "follow-redirects" property:

  • quarkus.rest-client.follow-redirects to enable redirection for all REST clients.

  • quarkus.rest-client.<client-prefix>.follow-redirects to enable redirection for a specific REST client.

如果该属性为 true,则 REST 客户端将执行它从 HTTP 服务器接收到重定向响应后执行的新请求。

If this property is true, then REST Client will perform a new request that it receives a redirection response from the HTTP server.

此外,我们可以使用“max-redirects”属性限制重定向次数。

Additionally, we can limit the number of redirections using the property "max-redirects".

一个重要的提示是,根据 RFC2616 规范,默认情况下,重定向将仅针对 GET 或 HEAD 方法发生。但是,在 REST Client 中,你可以提供自定义重定向处理程序以启用对 POST 或 PUT 方法的重定向,或在使用 @ClientRedirectHandler 注释、CDI 或在创建客户端时以编程方式遵循更复杂的逻辑。

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.

让我们看一个示例,了解如何使用 @ClientRedirectHandler 注释注册自定义重定向处理程序:

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

“alwaysRedirect”重定向处理程序将仅由指定的 REST Client 使用,在本例中是“ExtensionsService”客户端。

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

或者,你还可以通过 CDI 为所有 REST 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;
        };
    }
}

现在,所有 REST Client 都将使用自定义重定向处理程序。

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.

Update the test

接下来,我们需要更新功能测试以反映对端点的更改。编辑 src/test/java/org/acme/rest/client/ExtensionsResourceTest.java 文件并将测试内容更改为:

Next, we need to update the functional test to reflect the changes made to the endpoint. Edit the src/test/java/org/acme/rest/client/ExtensionsResourceTest.java file and change the content of the test to:

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功能。

The code above uses REST Assured's json-path capabilities.

Async Support

为了充分发挥客户端的反应式特性,你可以使用非阻塞风格的 REST Client 扩展,它支持 CompletionStageUni。让我们通过在 ExtensionsService REST 接口中添加 getByIdAsync 方法来看到它的实际操作。代码应如下所示:

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`文件并用以下内容更新它:

Open the src/main/java/org/acme/rest/client/ExtensionsResource.java file and update it with the following content:

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

请注意,由于调用现在是非阻塞的,因此 idAsync 方法将在事件循环上调用,即不会卸载到工作池线程,从而减少硬件资源利用率。有关详细信息,请参见 Quarkus REST execution model

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`中添加以下测试方法:

To test asynchronous methods, add the test method below in 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`版本非常相似:

The Uni version is very similar:

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`变为:

The ExtensionsResource becomes:

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 reactive 类型。如果不熟悉 Mutiny,请参阅 Mutiny - an intuitive reactive programming library

The previous snippet uses Mutiny reactive types. If you are not familiar with Mutiny, check Mutiny - an intuitive reactive programming library.

在返回 Uni`时,每个 _subscription_都会调用远程服务。这意味着可以通过重新订阅 `Uni`重新发送请求,或按如下方式使用 `retry

When returning a Uni, every subscription invokes the remote service. It means you can re-send the request by re-subscribing on the Uni, or use a retry as follows:

@RestClient ExtensionsService extensionsService;

// ...

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

如果使用 CompletionStage,则需要调用服务的方法来重试。这种差异源自 Mutiny 的惰性方面及其订阅协议。有关这方面的更多详细信息,请参阅 the Mutiny documentation

If you use a CompletionStage, you would need to call the service’s method to retry. This difference comes from the laziness aspect of Mutiny and its subscription protocol. More details about this can be found in the Mutiny documentation.

Server-Sent Event (SSE) support

只需将结果类型声明为 io.smallrye.mutiny.Multi 即可使用 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();
}

用于流化 SSE 结果的所有 IO 都以非阻塞方式完成。

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

结果不限于字符串 - 例如,当服务器为每个事件返回 JSON 有效负载时,Quarkus 会自动将其反序列化为 Multi 中使用的通用类型。

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.

用户还可以使用 org.jboss.resteasy.reactive.client.SseEvent 类型访问整个 SSE 事件。

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

一个简单的示例,其中事件有效负载是 Long 值如下:

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

有时,SSE 事件流可能包含某些不应该由客户端返回的事件 - 一个例子是服务器发送心跳事件以保持底层 TCP 连接打开。REST Client 支持通过提供 @org.jboss.resteasy.reactive.client.SseEventFilter 来过滤掉此类事件。

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

Custom headers support

有几种方法可以为 REST 调用指定自定义标头:

There are a few ways in which you can specify custom headers for your REST calls:

  • by registering a ClientHeadersFactory or a ReactiveClientHeadersFactory with the @RegisterClientHeaders annotation

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

  • by specifying the value of the header with @ClientHeaderParam

  • by specifying the value of the header by @HeaderParam

下面的代码演示了如何使用每个技术:

The code below demonstrates how to use each of these techniques:

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 There can be only one ClientHeadersFactory per class. With it, you can not only add custom headers, but you can also transform existing ones. See the RequestUUIDHeaderFactory class below for an example of the factory.
2 @ClientHeaderParam can be used on the client interface and on methods. It can specify a constant header value…​
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 …​ as well as a value from your application’s configuration
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 时,如果要利用默认方法,则需要将 Kotlin 编译器配置为使用 Java 的默认接口功能。有关更多详细信息,请参阅 this

When using Kotlin, if default methods are going to be leveraged, then the Kotlin compiler needs to be configured to use Java’s default interface capabilities. See this for more details.

ClientHeadersFactory 可能如下所示:

A ClientHeadersFactory can look as follows:

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。

As you see in the example above, you can make your ClientHeadersFactory implementation a CDI bean by annotating it with a scope-defining annotation, such as @Singleton, @ApplicationScoped, etc.

若要指定 ${header.value} 的值,只需在 application.properties 中放置以下内容:

To specify a value for ${header.value}, simply put the following in your application.properties:

header.value=value of the header

此外,有一个 ClientHeadersFactory 的反应型味道,它允许执行阻塞操作。例如:

Also, there is a reactive flavor of ClientHeadersFactory that allows doing blocking operations. For example:

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

使用 HTTP Basic Auth 时,@io.quarkus.rest.client.reactive.ClientBasicAuth 标注提供了一种更简单的方法来配置必要的 Authorization 标头。

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 {

}

其中 service.usernameservice.password 是配置属性,必须在运行时将其设置为允许访问所调用服务的用户名和密码。

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.

Default header factory

@RegisterClientHeaders 注释也可以在没有任何指定自定义工厂的情况下使用。在这种情况下,将使用 DefaultClientHeadersFactoryImpl 工厂。如果您从 REST 资源中发出 REST 客户端调用,则此工厂会将 org.eclipse.microprofile.rest.client.propagateHeaders 配置属性中列出的所有标头从资源请求传播到客户端请求。各个标头名称用逗号分隔。

The @RegisterClientHeaders annotation can also be used without any custom factory specified. In that case the DefaultClientHeadersFactoryImpl factory will be used. If you make a REST client call from a REST resource, this factory will propagate all the headers listed in org.eclipse.microprofile.rest.client.propagateHeaders configuration property from the resource request to the client request. Individual header names are comma-separated.

@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

Customizing the request

REST 客户端通过过滤器支持进一步自定义发送到服务器的最终请求。过滤器必须实现接口 ClientRequestFilterResteasyReactiveClientRequestFilter

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

接下来,你可以使用 @RegisterProvider 注解注册你的过滤器:

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

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

    // ...
}

或使用 .register() 方法以编程方式注册:

Or programmatically using the .register() method:

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

Injecting the jakarta.ws.rs.ext.Providers instance in filters

当我们需要查找当前客户端的提供程序实例时,jakarta.ws.rs.ext.Providers 很有用。

The jakarta.ws.rs.ext.Providers is useful when we need to lookup the provider instances of the current client.

我们可以从请求上下文中获取过滤器中的 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();
        // ...
    }
}

或者,你也可以实现 ResteasyReactiveClientRequestFilter 接口代替 ClientRequestFilter 接口,该接口将直接提供 ResteasyReactiveClientRequestContext 上下文:

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

REST 客户端支持添加一个自定义 ObjectMapper,仅使用 @ClientObjectMapper 注解对客户端进行操作。

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

一个简单的示例是通过执行以下操作向 REST 客户端 Jackson 扩展提供一个自定义 ObjectMapper:

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.

Exception handling

MicroProfile REST 客户端规范引入了 org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper,其目的是将 HTTP 响应转换为异常。

The MicroProfile REST Client specification introduces the org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper whose purpose is to convert an HTTP response to an exception.

一个简单的示例是为上面讨论的 ExtensionsService 实现这样的 ResponseExceptionMapper

A simple example of implementing such a ResponseExceptionMapper for the ExtensionsService discussed above, could be:

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 还定义了 getPriority 方法,该方法用于确定调用 ResponseExceptionMapper 实现的优先级(对于 getPriority 值较低的实现,将首先调用)。如果 toThrowable 返回异常,则将抛出该异常。如果返回 null,则将调用链中的下一个 ResponseExceptionMapper 实现(如果存在)。

ResponseExceptionMapper also defines the getPriority method which is used in order to determine the priority with which ResponseExceptionMapper implementations will be called (implementations with a lower value for getPriority will be invoked first). If toThrowable returns an exception, then that exception will be thrown. If null is returned, the next implementation of ResponseExceptionMapper in the chain will be called (if there is any).

如上所述,任何 REST 客户端都不会自动使用该类。为了使其可供应用程序的每个 REST 客户端使用,该类需要用 @Provider 进行注释(只要 quarkus.rest-client-reactive.provider-autodiscovery 未设置为 false )。或者,如果异常处理类仅应适用于特定的 REST 客户端接口,则可以通过 @RegisterProvider(MyResponseExceptionMapper.class) 对接口进行注释或使用 providers 配置组的 quarkus.rest-client 属性使用配置进行注册。

The class as written above, would not be automatically be used by any REST Client. To make it available to every REST Client of the application, the class needs to be annotated with @Provider (as long as quarkus.rest-client-reactive.provider-autodiscovery is not set to false). Alternatively, if the exception handling class should only apply to specific REST Client interfaces, you can either annotate the interfaces with @RegisterProvider(MyResponseExceptionMapper.class), or register it using configuration using the providers property of the proper quarkus.rest-client configuration group.

Using @ClientExceptionMapper

可以使用 @ClientExceptionMapper 注解以更简单的方式转换大于或等于 400 的 HTTP 响应代码。

A simpler way to convert HTTP response codes of 400 or above is to use the @ClientExceptionMapper annotation.

对于上面定义的 ExtensionsService REST Client 接口,@ClientExceptionMapper 的示例用途如下:

For the ExtensionsService REST Client interface defined above, an example use of @ClientExceptionMapper would be:

@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 Client。如果 priority 属性未设置,则 @ClientExceptionMapper 会使用默认优先级,并且仍适用于依次调用所有处理器的正常规则。

Naturally this handling is per REST Client. @ClientExceptionMapper uses the default priority if the priority attribute is not set and the normal rules of invoking all handlers in turn apply.

使用 @ClientExceptionMapper 进行注解的方法还可以采用 java.lang.reflect.Method 参数,当异常映射代码需要得知 REST Client 方法(调用时导致异常映射代码接合)时,这种方式非常有用。

Methods annotated with @ClientExceptionMapper can also take a java.lang.reflect.Method parameter which is useful if the exception mapping code needs to know the REST Client method that was invoked and caused the exception mapping code to engage.

Using @Blocking annotation in exception mappers

在需要将 InputStream 用于 REST Client 方法的返回类型的情况下(例如在需要读取大量数据时):

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

这会按预期运行,但是,如果您尝试在自定义异常映射器中读取此 InputStream 对象,您将会收到一个 BlockingNotAllowedException 异常。这是因为 ResponseExceptionMapper 类默认在事件循环线程执行程序上运行 - 而它不允许执行 IO 操作。

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.

要让您的异常映射器成为阻塞模式,您可以使用 @Blocking 注解对该异常映射器进行注解:

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 With the @Blocking annotation, the MyResponseExceptionMapper exception mapper will be executed in the worker thread pool.
2 Reading the entity is now allowed because we’re executing the mapper on the worker thread pool.

请注意,您还可以在使用 @ClientExceptionMapper 时使用 @Blocking 注解:

Note that you can also use the @Blocking annotation when using @ClientExceptionMapper:

@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;
    }
}

Multipart Form support

Sending Multipart messages

REST Client 允许以 multipart 表单发送数据。这样您可以高效地发送文件。

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

要以 multipart 表单发送数据,您只需要使用常规 @RestForm (或 @FormParam)注解:

To send data as a multipart form, you can just use the regular @RestForm (or @FormParam) annotations:

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

指定为 FilePathbyte[]BufferFileUpload 的参数将作为文件发送,并且默认使用 application/octet-stream MIME 类型。其他 @RestForm 参数类型默认使用 text/plain MIME 类型。您可以使用 @PartType 注解覆盖这些默认设置。

Parameters specified as File, Path, byte[], Buffer or FileUpload are sent as files and default to the application/octet-stream MIME type. Other @RestForm parameter types default to the text/plain MIME type. You can override these defaults with the @PartType annotation.

当然,您还可以将这些参数分组到一个包含类中:

Naturally, you can also group these parameters into a containing class:

    public static class Parameters {
        @RestForm
        File file;

        @RestForm
        String otherField;
    }

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

任何 @RestFormFilePathbyte[]BufferFileUpload 类型的参数,以及任何使用 @PartType 进行注解的参数,在方法中都会自动暗示一个 @Consumes(MediaType.MULTIPART_FORM_DATA),前提是该方法中没有 @Consumes

Any @RestForm parameter of the type File, Path, byte[], Buffer or FileUpload, as well as any annotated with @PartType automatically imply a @Consumes(MediaType.MULTIPART_FORM_DATA) on the method if there is no @Consumes present.

如果存在不是暗示 multipart 的 @RestForm 参数,则会暗示 @Consumes(MediaType.APPLICATION_FORM_URLENCODED)

If there are @RestForm parameters that are not multipart-implying, then @Consumes(MediaType.APPLICATION_FORM_URLENCODED) is implied.

有几种模式可以用来对表单数据进行编码。默认情况下,REST Client 使用 RFC1738。您可以通过在客户端级别指定模式来覆盖它,方法是将 io.quarkus.rest.client.multipart-post-encoder-mode RestBuilder 属性设置为所选的 HttpPostRequestEncoder.EncoderMode 值,或者在 application.properties 中指定 quarkus.rest-client.multipart-post-encoder-mode。请注意,后者仅适用于使用 @RegisterRestClient 注解创建的客户端。所有可用的模式都在 Netty documentation 中进行了说明。

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 multipart:

You can also send JSON multiparts by specifying the @PartType annotation:

    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

在需要通过编程对 multipart 内容进行构建的情况下,REST Client 提供了 ClientMultipartForm,它可以在 REST Client 中像这样使用:

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

有关此类受支持方法的更多信息,请参见 ClientMultipartForm 的 javadoc。

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

创建 ClientMultipartForm 的一个好例子是,从服务器的 MultipartFormDataInput 创建它(表示 Quarkus REST 收到的多部分请求) - 其目的是传播下游请求,同时允许进行任意修改:

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

则可以轻松地创建 ClientMultipartForm,如下所示:

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.fileUpload(fu); (3)
  });
  return multiPartForm;
}
1 Request representing the request the server parts accepts
2 A jsonPayload attribute is added directly to ClientMultipartForm
3 A fileUpload is created from the request’s FileUpload

如果客户端和服务器不使用同一多部分编码器模式,则在发送使用相同名称的多部分数据时可能会出现问题。默认情况下,REST 客户端使用 RFC1738,但根据具体情况,客户端可能需要使用 HTML5RFC3986 模式进行配置。

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.

此配置可通过 quarkus.rest-client.multipart-post-encoder-mode 属性实现。

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

Receiving Multipart Messages

REST 客户端还支持接收多部分消息。与发送类似,若要解析多部分响应,您需要创建一个描述响应数据的类,例如:

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 uses the shorthand @RestForm annotation to make a field as a part of a multipart form
2 the standard @FormParam can also be used. It allows to override the name of the multipart part.

然后,创建一个与调用相对应的界面方法,并使其返回 FormDto

Then, create an interface method that corresponds to the call and make it return the FormDto:

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

目前,多部分响应支持受到以下限制:

At the moment, multipart response support is subject to the following limitations:

  • files sent in multipart responses can only be parsed to File, Path and FileDownload

  • each field of the response type has to be annotated with @PartType - fields without this annotation are ignored

REST 客户端需要预先知道用作多部分返回类型的类。如果你具有生成 `multipart/form-data`的接口方法,则会自动发现返回类型。但是,如果你打算使用 `ClientBuilder`API 将响应解析为多部分,则需要用 `@MultipartForm`注释 DTO 类。

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.

你下载的文件不会自动删除,并且可能占用大量磁盘空间。考虑在完成处理后删除文件。

The files you download are not automatically removed and can take up a lot of disk space. Consider removing the files when you are done working with them.

Multipart mixed / OData usage

应用程序必须使用称为 OData的特殊协议与企业系统(如 CRM 系统)交互并不罕见。此协议实质上使用自定义 HTTP Content-Type,该协议需要一些粘合代码才能与 REST 客户端一起使用(创建正文完全取决于应用程序 - REST 客户端帮不了什么忙)。

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

示例如下所示:

An example looks like the following:

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

如前所述,正文参数需要由应用程序代码正确设计,以符合服务的 requirement。

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

Receiving compressed messages

REST 客户端还支持使用 GZIP 接收压缩消息。你可以通过添加属性 `quarkus.http.enable-compression=true`来启用 HTTP 压缩支持。当启用此功能且服务器返回包含标头 `Content-Encoding: gzip`的响应时,REST 客户端将自动解码内容并继续处理消息。

REST Client also supports receiving compressed messages using GZIP. You can enable the HTTP compression support by adding the property quarkus.http.enable-compression=true. When this feature is enabled and a server returns a response that includes the header Content-Encoding: gzip, REST Client will automatically decode the content and proceed with the message handling.

Proxy support

REST 客户端支持通过代理发送请求。它遵循其 JVM 设置,但也允许同时指定:

REST Client supports sending requests through a proxy. It honors the JVM settings for it but also allows to specify both:

  • global client proxy settings, with quarkus.rest-client.proxy-address, quarkus.rest-client.proxy-user, quarkus.rest-client.proxy-password, quarkus.rest-client.non-proxy-hosts

  • per-client proxy settings, with quarkus.rest-client.<my-client>.proxy-address, etc. These are applied only to clients injected with CDI, that is the ones created with @RegisterRestClient

如果在客户端级别设置 proxy-address,则客户端使用其特定代理设置。不会从全局配置或 JVM 属性传播任何代理设置。

If proxy-address is set on the client level, the client uses its specific proxy settings. No proxy settings are propagated from the global configuration or JVM properties.

如果未为客户端设置 `proxy-address`但在全局级别设置了它,则客户端使用全局设置。否则,客户端使用 JVM 设置。

If proxy-address is not set for the client but is set on the global level, the client uses the global settings. Otherwise, the client uses the JVM settings.

设置代理的示例配置:

An example configuration for setting proxy:

# 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 客户端规范不允许设置代理凭据。为了以编程方式指定代理用户和代理密码,你需要将 RestClientBuilder`强制转换为 `RestClientBuilderImpl

MicroProfile REST Client specification does not allow setting proxy credentials. In order to specify proxy user and proxy password programmatically, you need to cast your RestClientBuilder to RestClientBuilderImpl.

Local proxy for dev mode

在开发模式下使用 REST 客户端时,Quarkus 可以建立一个传递代理,该代理可以用作 Wireshark(或类似工具)的目标,以便捕获源自 REST 客户端的所有流量(当 REST 客户端用于 HTTPS 服务时,这非常有意义)。

When using the REST Client in dev mode, Quarkus has the ability to stand up a pass-through proxy which can be used as a target for Wireshark (or similar tools) in order to capture all the traffic originating from the REST Client (this really makes sense when the REST Client is used against HTTPS services)

要启用此功能,需要做的就是为需要代理的客户端的 configKey 设置 enable-local-proxy 配置选项。例如:

To enable this feature, all that needs to be done is set the enable-local-proxy configuration option for the configKey corresponding to the client for which proxying is desired. For example:

quarkus.rest-client.my-client.enable-local-proxy=true

当 REST 客户端不使用 config 密钥时(例如通过 QuarkusRestClientBuilder 以编程方式创建时),可以使用类名称代替。例如:

When a REST Client does not use a config key (for example when it is created programmatically via QuarkusRestClientBuilder) then the class name can be used instead. For example:

quarkus.rest-client."org.acme.SomeClient".enable-local-proxy=true

监听代理的端口可以在启动日志中找到。一个示例条目是:

The port the proxy is listening can be found in startup logs. An example entry is:

Started HTTP proxy server on http://localhost:38227 for REST Client 'org.acme.SomeClient'

Package and run the application

使用以下内容运行应用程序:

Run the application with:

Unresolved directive in rest-client.adoc - include::{includes}/devtools/dev.adoc[]

使用 [role="bare"][role="bare"]http://localhost:8080/extension/id/io.quarkus:quarkus-rest-client 打开你的浏览器。

您应该会看到一个包含此扩展程序的一些基本信息 JSON 对象。

You should see a JSON object containing some basic information about this extension.

和往常一样,可以使用以下命令打包应用程序:

As usual, the application can be packaged using:

Unresolved directive in rest-client.adoc - include::{includes}/devtools/build.adoc[]

并且使用 java -jar target/quarkus-app/quarkus-run.jar 执行。

And executed with java -jar target/quarkus-app/quarkus-run.jar.

你还可以按如下方式生成本机可执行文件:

You can also generate the native executable with:

Unresolved directive in rest-client.adoc - include::{includes}/devtools/build-native.adoc[]

Logging traffic

REST 客户端可以记录它发送的请求和它接收的响应。要启用记录,请将 quarkus.rest-client.logging.scope 属性添加到您的 application.properties 中并将其设置为:

REST Client can log the requests it sends and the responses it receives. To enable logging, add the quarkus.rest-client.logging.scope property to your application.properties and set it to:

  • request-response to log the request and response contents, or

  • all to also enable low level logging of the underlying libraries.

由于 HTTP 消息可能具有较大的主体,因此我们限制了记录的主体字符数。默认限制为 100,您可以通过指定 quarkus.rest-client.logging.body-limit 来更改它。

As HTTP messages can have large bodies, we limit the amount of body characters logged. The default limit is 100, you can change it by specifying quarkus.rest-client.logging.body-limit.

REST 客户端使用 DEBUG 级别记录流量,并且不更改记录器属性。您可能需要调整记录器配置以使用此功能。

REST Client is logging the traffic with level DEBUG and does not alter logger properties. You may need to adjust your logger configuration to use this feature.

一个示例记录配置:

An example logging configuration:

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 客户端使用默认 ClientLogger 实现,该实现可以换成自定义实现。

REST Client uses a default ClientLogger implementation, which can be swapped out for a custom implementation.

使用 QuarkusRestClientBuilder 以编程方式设置客户端时,可以通过 clientLogger 方法设置 ClientLogger

When setting up the client programmatically using the QuarkusRestClientBuilder, the ClientLogger is set via the clientLogger method.

对于使用 @RegisterRestClient 的声明式客户端,只需提供一个实现了 ClientLogger 的 CDI Bean,就足以让所述客户端使用该记录器。

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

Mocking the client for tests

如果您使用使用 @RestClient 注解注入的客户端,则可以轻松地对其进行模拟以进行测试。您可以使用 Mockito 的 @InjectMockQuarkusMock 来执行此操作。

If you use a client injected with the @RestClient annotation, you can easily mock it for tests. You can do it with Mockito’s @InjectMock or with QuarkusMock.

本节说明如何用模拟替换客户端。如果您希望更深入地了解 Quarkus 中的模拟工作方式,请参阅 Mocking CDI beans 上的博客文章。

This section shows how to replace your client with a mock. If you would like to get a more in-depth understanding of how mocking works in Quarkus, see the blog post on Mocking CDI beans.

在使用 @QuarkusIntegrationTest 时,模拟不起作用。

Mocking does not work when using @QuarkusIntegrationTest.

我们假设您有以下客户端:

Let’s assume you have the following client:

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

Mocking with InjectMock

用 Mockito 和 @InjectMock 模拟测试客户端的最简单方法是。

The simplest approach to mock a client for tests is to use Mockito and @InjectMock.

首先,向你的应用添加以下依赖:

First, add the following dependency to your application:

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 来创建并注入模拟:

Then, in your test you can simply use @InjectMock to create and inject a mock:

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.InjectMock;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class InjectMockTest {

    @InjectMock
    @RestClient
    Client mock;

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

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

Mocking with QuarkusMock

如果 Mockito 不能满足你的需求,你可以通过使用 QuarkusMock 对模拟进行编码,例如:

If Mockito doesn’t meet your needs, you can create a mock programmatically using QuarkusMock, e.g.:

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 here we use a manually created implementation of the client interface to replace the actual Client
2 note that RestClient.LITERAL has to be passed as the last argument of the installMockForType method

Using a Mock HTTP Server for tests

建立一个模拟的 HTTP 服务器,针对它运行测试,这是常见的测试模式。此类服务器的示例包括 WiremockHoverfly。在此部分我们将演示 Wiremock 如何用于测试在上面开发的 ExtensionsService

Setting up a mock HTTP server, against which tests are run, is a common testing pattern. Examples of such servers are Wiremock and Hoverfly. In this section we’ll demonstrate how Wiremock can be leveraged for testing the ExtensionsService which was developed above.

首先,需要将 Wiremock 添加为测试依赖。对于 Maven 项目,它会这样发生:

First, Wiremock needs to be added as a test dependency. For a Maven project that would happen like so:

pom.xml
<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
    <version>${wiremock.version}</version> 1
</dependency>
1 Use a proper Wiremock version. All available versions can be found here.
build.gradle
testImplementation("org.wiremock:wiremock:$wiremockVersion") 1
1 Use a proper Wiremock version. All available versions can be found here.

在 Quarkus 测试中,当某些服务需要在 Quarkus 测试运行之前启动,我们使用 @io.quarkus.test.common.WithTestResource 注解指定一个 io.quarkus.test.common.QuarkusTestResourceLifecycleManager,它能启动服务并提供 Quarkus 将使用的配置值。

In Quarkus tests when some service needs to be started before the Quarkus tests are ran, we utilize the @io.quarkus.test.common.WithTestResource annotation to specify a io.quarkus.test.common.QuarkusTestResourceLifecycleManager which can start the service and supply configuration values that Quarkus will use.

有关 @WithTestResource 的更多详情,请参阅 this part of the documentation

For more details about @WithTestResource refer to this part of the documentation.

让我们像这样创建一个 QuarkusTestResourceLifecycleManager 的实现,名为 WiremockExtensions

Let’s create an implementation of QuarkusTestResourceLifecycleManager called WiremockExtensions like so:

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 Statically importing the methods in the Wiremock package makes it easier to read the test.
2 The start method is invoked by Quarkus before any test is run and returns a Map of configuration properties that apply during the test execution.
3 Launch Wiremock.
4 Configure Wiremock to stub the calls to /extensions?id=io.quarkus:quarkus-rest-client by returning a specific canned response.
5 All HTTP calls that have not been stubbed are handled by calling the real service. This is done for demonstration purposes, as it is not something that would usually happen in a real test.
6 As the start method returns configuration that applies for tests, we set the rest-client property that controls the base URL which is used by the implementation of ExtensionsService to the base URL where Wiremock is listening for incoming requests.
7 When all tests have finished, shutdown Wiremock.

ExtensionsResourceTest 测试类需要像这样添加注解:

The ExtensionsResourceTest test class needs to be annotated like so:

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

}

Known limitations

虽然 REST 客户端扩展旨在替换 RESTEasy 客户端扩展,但它们之间存在一些差异和限制:

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.

  • it is not possible to set HostnameVerifier or SSLContext

  • a few things that don’t make sense for a non-blocking implementations, such as setting the ExecutorService, don’t work