Using the REST Client

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

  • 安装了 JDK 17+,已正确配置 JAVA_HOME

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Solution

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

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

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

Creating the Maven project

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

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

适用于 Windows 用户:

  • 如果使用 cmd,(不要使用反斜杠 \ ,并将所有内容放在同一行上)

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

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

  • REST 服务器支持的`rest-jackson`扩展。如果您不想使用 Jackson,请改用`rest`;

  • REST 客户端支持的`rest-client-jackson`扩展。如果您不想使用 Jackson,请改用`rest-client`

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

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

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

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 的形式。

创建一个 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;

}

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

Create the interface

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

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

  • @RegisterRestClient 允许 Quarkus 知道此接口旨在作为 REST 客户端用于 CDI 注入

  • @Path@GET@QueryParam 是用于定义如何访问服务的标准 Jakarta REST 注释

当安装`quarkus-rest-client-jackson`扩展时,除非通过`@Produces`或`@Consumes`注释明确设置媒体类型,否则 Quarkus 将对大多数返回值默认使用`application/json`媒体类型。 如果你不依赖于 JSON 默认值,强烈建议使用 @Produces@Consumes 注释为端点添加注释,以精确定义预期 Content-Type。这将使你可以减少原生可执行文件中包含的 Jakarta REST 提供程序(可以看作转换器)的数量。

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

Query Parameters

指定查询参数的最简单方法是用 @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 条目正好表示一个查询参数
3 MultivaluedMap 允许你发送数组值

Using @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`将被添加到客户端的所有请求。因为我们使用了 `${&#8230;&#8203;} 语法,所以将使用 my.property-value 配置属性获取参数的实际值。
2 调用 getWithOtherParam 时,除了 my-param 查询参数外,还会添加值为 othersome-other-param
3 调用 getFromMethod 时,除了 my-param 查询参数外,还会添加值为 testparam-from-method(因为这是调用 param-from-methodwithParam 方法返回的内容)。

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

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

Form Parameters

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

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

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

Path Parameters

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

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:

  • InputStream

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

此外,如果使用以下类型之一,客户端也可以发送任意大的文件:

  • File

  • Path

Create the configuration

要确定发起 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 有此配置意味着使用 org.acme.rest.client.ExtensionsService 执行的所有请求将使用 https://stage.code.quarkus.io/api 作为基本 URL。使用上述配置调用带有值 io.quarkus:quarkus-rest-clientExtensionsServicegetById 方法,将导致对 https://stage.code.quarkus.io/api/extensions?id=io.quarkus:quarkus-rest-client 发出 HTTP GET 请求。

请注意,org.acme.rest.client.ExtensionsService must 匹配我们之前部分中创建的 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

Disabling Hostname Verification

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

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

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

HTTP/2 Support

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

// 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。如果你想启用它,你可以设置:

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 文件:

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

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

1 将客户端存根注入 @RestClient 注释,而不是常见的 CDI @Inject

Programmatic client creation with QuarkusRestClientBuilder

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

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

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

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

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 客户端扩展允许通过属性配置一些设置,例如:

  • quarkus.rest-client.client-prefix.connect-timeout 以毫秒为单位配置连接超时。

  • quarkus.rest-client.client-prefix.max-redirects 以限制重定向次数。

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

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

我们来看一个通过 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 如果存在,客户端将使用已注册的 HTTP 客户端选项而不是通过 CDI 提供的 HTTP 客户端选项。

Redirection

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

  • `quarkus.rest-client.follow-redirects`可为所有 REST 客户端启用重定向。

  • `quarkus.rest-client.&lt;client-prefix&gt;.follow-redirects`可为特定 REST 客户端启用重定向。

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

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

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

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

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

或者,你还可以通过 CDI 为所有 REST Client 提供自定义重定向处理程序:

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 都将使用自定义重定向处理程序。

另一种方法是在创建客户端时以编程方式提供它:

@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 如果存在,客户端将使用注册的重定向处理程序,而不是通过 CDI 提供的重定向处理程序。

Update the test

接下来,我们需要更新功能测试以反映对端点的更改。编辑 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功能。

Async Support

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

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

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

要测试异步方法,请在 `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 reactive 类型。如果不熟悉 Mutiny,请参阅 Mutiny - an intuitive reactive programming library

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

@RestClient ExtensionsService extensionsService;

// ...

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

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

Server-Sent Event (SSE) support

只需将结果类型声明为 io.smallrye.mutiny.Multi 即可使用 SSE 事件。

最简单的例子是:

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 都以非阻塞方式完成。

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

用户还可以使用 org.jboss.resteasy.reactive.client.SseEvent 类型访问整个 SSE 事件。 一个简单的示例,其中事件有效负载是 Long 值如下:

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 来过滤掉此类事件。

以下是一个筛选心率事件的示例:

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 调用指定自定义标头:

  • 通过使用 ClientHeadersFactory 标注注册 ReactiveClientHeadersFactory@RegisterClientHeaders

  • 通过使用 ClientHeadersFactory 方法,使用编程方式注册 ReactiveClientHeadersFactoryQuarkusRestClientBuilder.clientHeadersFactory(factory)

  • 通过使用 @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。使用它,你不仅可以添加自定义标头,还可以转换现有标头。有关该工厂的示例,请参阅下面的 RequestUUIDHeaderFactory 类。
2 @ClientHeaderParam 可用于客户端界面和方法。可以指定一个固定标头值…​
3 …​以及应该计算标头值的方法名称。它可以是该界面中的一个静态方法或一个默认方法。该方法可以不带任何参数,可以带单个 String 参数,也可以带单个 io.quarkus.rest.client.reactive.ComputedParamContext 参数(这对需要根据方法参数计算标头的代码非常有用,并且自然地补充了 @io.quarkus.rest.client.reactive.NotBody)。
4 …​以及来自应用程序配置的值
5 …​甚至可以是逐字文本、方法参数(通过名称引用)、配置值(如前所述)和方法调用的任何组合(如前所述)
6 …​或作为普通的 Jakarta REST @HeaderParam 标注参数

在使用 Kotlin 时,如果要利用默认方法,则需要将 Kotlin 编译器配置为使用 Java 的默认接口功能。有关更多详细信息,请参阅 this

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

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

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

}

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

Default header factory

@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

Customizing the request

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

自定义请求的一个简单示例是添加自定义标头:

@Provider
public class TestClientRequestFilter implements ClientRequestFilter {

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

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

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

    // ...
}

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

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

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

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

我们可以从请求上下文中获取过滤器中的 Providers 实例,如下所示:

@Provider
public class TestClientRequestFilter implements ClientRequestFilter {

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

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

@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 注解对客户端进行操作。

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

@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 该方法必须用 @ClientObjectMapper 进行注释。
2 它必须是一个静态方法。此外,参数 defaultObjectMapper 将通过 CDI 进行解析。如果未找到,它将在运行时抛出异常。
3 在此示例中,我们正在创建默认对象映射程序的副本。你应该 NEVER 修改默认对象映射程序,但创建副本。

Exception handling

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

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

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

Using @ClientExceptionMapper

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

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

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

Using @Blocking annotation in exception mappers

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

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

    @GET
    InputStream get();
}

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

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

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

Multipart Form support

Sending Multipart messages

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

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

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

指定为 FilePathbyte[]BufferFileUpload 的参数将作为文件发送,并且默认使用 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);

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

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

有几种模式可以用来对表单数据进行编码。默认情况下,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 中进行了说明。

您还可以通过指定 @PartType 注解来发送 JSON multipart:

    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 中像这样使用:

public interface MultipartService {

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

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

Converting a received multipart object into a client request

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

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 是一个表示接收的多部分请求的 Quarkus REST(服务器)类型。
2 A ClientMultipartForm is created.
3 为表示内存中文件属性的请求属性创建了 FileItem 属性
4 为表示保存在文件系统上的文件属性的请求属性创建了 FileItem 属性
5 未将文件属性直接添加到 ClientMultipartForm 中(如果不是 FileItem)。

以类似的方式,如果已知接收的服务器多部分请求看起来像:

public class Request { (1)

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

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

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

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
2 将一个 jsonPayload 直接添加到 ClientMultipartForm
3 从请求的 FileUpload 创建 fileUpload

如果客户端和服务器不使用同一多部分编码器模式,则在发送使用相同名称的多部分数据时可能会出现问题。默认情况下,REST 客户端使用 RFC1738,但根据具体情况,客户端可能需要使用 HTML5RFC3986 模式进行配置。 此配置可通过 quarkus.rest-client.multipart-post-encoder-mode 属性实现。

Receiving Multipart Messages

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

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

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

Multipart mixed / OData usage

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

示例如下所示:

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

该代码使用以下部分:

1 @ClientHeaderParam(name = "Content-Type", value = "{calculateContentType}"),它通过调用接口的 `Content-Type`默认方法确保创建了 `calculateContentType`标头。
2 上述参数需要用 `@NotBody`注释,因为它仅用于帮助构建 HTTP 标头。
3 context.methodParameters().get(1).value(),它允许 `calculateContentType`方法获取传递给 REST 客户端方法的适当方法参数。

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

Receiving compressed messages

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

Proxy support

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

  • 具有 quarkus.rest-client.proxy-addressquarkus.rest-client.proxy-userquarkus.rest-client.proxy-password、`quarkus.rest-client.non-proxy-hosts`的全局客户端代理设置

  • 具有 `quarkus.rest-client.&lt;my-client&gt;.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 客户端规范不允许设置代理凭据。为了以编程方式指定代理用户和代理密码,你需要将 RestClientBuilder`强制转换为 `RestClientBuilderImpl

Local proxy for dev mode

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

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

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

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

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

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

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

Package and run the application

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

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

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

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

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

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

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

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

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

Logging traffic

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

  • 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 客户端使用默认 ClientLogger 实现,该实现可以换成自定义实现。 使用 QuarkusRestClientBuilder 以编程方式设置客户端时,可以通过 clientLogger 方法设置 ClientLogger。 对于使用 @RegisterRestClient 的声明式客户端,只需提供一个实现了 ClientLogger 的 CDI Bean,就足以让所述客户端使用该记录器。

Mocking the client for tests

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

本节说明如何用模拟替换客户端。如果您希望更深入地了解 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();
}

Mocking with 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.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 对模拟进行编码,例如:

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 此处我们使用客户端接口的手动创建的实现来替换实际的 Client
2 请注意,RestClient.LITERAL 必须作为 installMockForType 方法的最后一个参数传递

Using a Mock HTTP Server for tests

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

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

pom.xml
<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
    <version>${wiremock.version}</version> 1
</dependency>
1 使用正确的 Wiremock 版本。所有可用的版本都可在 here 找到。
build.gradle
testImplementation("org.wiremock:wiremock:$wiremockVersion") 1
1 使用正确的 Wiremock 版本。所有可用的版本都可在 here 找到。

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

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

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

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 Launch Wiremock.
4 配置 Wiremock,使其对 /extensions?id=io.quarkus:quarkus-rest-client 的调用返回特定罐头响应。
5 所有未被截断的 HTTP 调用都通过调用真实服务来处理。这是为了演示目的而做的,因为在真实的测试中通常不会发生这种情况。
6 start 方法返回适用于测试的配置时,我们将控制 ExtensionsService 实现使用的基础 URL 的 rest-client 属性的设置,设置成 Wiremock 监听传入请求的基础 URL。
7 当所有测试都完成时,关闭 Wiremock。

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

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

}

Known limitations

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

  • 新扩展的客户端默认作用域为 @ApplicationScoped,而 quarkus-resteasy-client 的默认作用域为 @Dependent。要更改此行为,将 quarkus.rest-client-reactive.scope 属性设置为完全限定的作用域名称。

  • 无法设置 HostnameVerifierSSLContext

  • 一些对非阻塞实现没有意义的内容(比如设置 ExecutorService)不起作用