Using the legacy REST Client

本指南是关于与 RESTEasy Classic 兼容的 REST 客户端的,直到 Quarkus 2.8,它一直是默认的 Jakarta REST(以前称为 JAX-RS)实现。 现在建议使用 Quarkus REST(以前称为 RESTEasy Reactive),它同样支持传统的阻塞工作负载和反应式工作负载。有关 Quarkus REST 的更多信息,请参阅 REST Client guide,对于服务器端,请参阅 introductory REST JSON guide 或更详细的 Quarkus REST guide

本指南解释了如何使用 RESTEasy REST 客户端与 REST API 进行交互,而几乎不需要任何工作。

如果您需要编写服务器 JSON REST APIs,还有其他指南。

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[存档]。

解决方案位于 resteasy-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 服务器支持的 resteasyresteasy-jackson 扩展;

  • 用于 REST 客户端支持的 resteasy-clientresteasy-client-jackson 扩展。

如果您已配置 Quarkus 项目,您可以通过在项目根目录中运行以下命令将 resteasy-clientresteasy-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

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-client</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-client-jackson</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-resteasy-client")
implementation("io.quarkus:quarkus-resteasy-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

使用 RESTEasy 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 org.jboss.resteasy.annotations.jaxrs.QueryParam;

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

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

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

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

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

当安装了 JSON 扩展(例如 quarkus-resteasy-client-jacksonquarkus-resteasy-client-jsonb)时,Quarkus 将默认对大多数返回值使用 application/json 媒体类型,除非通过 @Produces@Consumes 注释显式设置媒体类型(有一些众所周知类型的例外,例如 StringFile,其分别默认为 text/plainapplication/octet-stream)。 如果你不想默认使用 JSON,你可以设置 quarkus.resteasy-json.default-json=false,默认值会变回自动协商。如果你设置这个,你将需要添加 @Produces(MediaType.APPLICATION_JSON)@Consumes(MediaType.APPLICATION_JSON) 到你的端点才能使用 JSON。 如果你不依赖于 JSON 默认值,强烈建议使用 @Produces@Consumes 注释为端点添加注释,以精确定义预期 Content-Type。这将使你可以减少原生可执行文件中包含的 Jakarta REST 提供程序(可以看作转换器)的数量。

Path Parameters

如果 GET 请求需要路径参数,你可以利用 @PathParam("parameter-name") 注释,而非(或附加) @QueryParam。路径和查询参数可以根据需要组合,如下面的模拟示例所示。

package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;

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

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

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

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)
quarkus.rest-client."org.acme.rest.client.ExtensionsService".scope=jakarta.inject.Singleton # (2)
1 使用此配置意味着使用 ExtensionsService 执行的所有请求都将使用 https://stage.code.quarkus.io 作为基本 URL。使用上述配置,用 io.quarkus:quarkus-resteasy-client 的值调用 ExtensionsServicegetById 方法将导致对 https://stage.code.quarkus.io/api/extensions?id=io.quarkus:quarkus-rest-client 发起 HTTP GET 请求。
2 使用此配置意味着 ExtensionsService 的默认作用域将是 @Singleton。受支持的作用域值分别是 @Singleton@Dependent@ApplicationScoped@RequestScoped。默认作用域是 @Dependent。还可以在界面上定义默认作用域。

请注意,org.acme.rest.client.ExtensionsService must 匹配我们之前部分中创建的 ExtensionsService 界面中的完整限定名。

也可以使用标准的 MicroProfile Rest 客户端属性表示法配置客户端:

org.acme.rest.client.ExtensionsService/mp-rest/url=https://stage.code.quarkus.io/api
org.acme.rest.client.ExtensionsService/mp-rest/scope=jakarta.inject.Singleton

如果通过 Quarkus 表示法和 MicroProfile 表示法指定一个属性,Quarkus 表示法具有优先权。

为了便于配置,你可以使用 @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 主机名验证。

此外,你可以将 REST 客户端配置为使用自定义主机名验证策略,只需提供实现 javax.net.ssl.HostnameVerifier 界面的类,并将以下属性添加到你的配置中:

quarkus.rest-client.extensions-api.hostname-verifier=<full qualified custom hostname verifier class name>

Quarkus REST 客户端提供了一个嵌入式主机名验证器策略,以禁用名为 io.quarkus.restclient.NoopHostnameVerifier 的主机名验证。

Disabling SSL verifications

要禁用所有 SSL 验证,请将以下属性添加到你的配置中:

quarkus.tls.trust-all=true

此设置不应在生产环境中使用,因为它会禁用任何类型的 SSL 验证。

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 org.jboss.resteasy.annotations.jaxrs.PathParam;

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

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

    @Inject
    @RestClient
    ExtensionsService extensionsService;

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

请注意除了标准 CDI @Inject`注解以外,还需要使用 MicroProfile `@RestClient`注解来注入 `ExtensionsService

Update the test

还需要更新功能测试来反映端点所做的更改。编辑 `src/test/java/org/acme/rest/client/ExtensionsResourceTest.java`文件并将 `testExtensionIdEndpoint`方法的内容更改为:

package org.acme.rest.client;

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

import org.acme.rest.client.resources.WireMockExtensionsResource;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
@WithTestResource(WireMockExtensionsResource.class)
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 Classic"),
                "[0].keywords.size()", greaterThan(1),
                "[0].keywords", hasItem("rest-client"));
    }
}

上面的代码使用的是 REST Assuredjson-path功能。

Redirection

HTTP 服务器可以通过发送一个以“3”开头的状态码和一个包含待重定向到的 URL 的 HTTP 标头“Location”的响应来重定向响应到另一个位置。当 REST 客户端从 HTTP 服务器接收到重定向响应时,它不会自动对新位置执行另一个请求。但是,可以通过启用“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 方法中。

Async Support

Rest 客户端支持异步 rest 调用。异步支持有 2 种类型:可以返回 CompletionStage`或 `Uni(需要 `quarkus-resteasy-client-mutiny`扩展)。让我们通过在 `ExtensionsService`REST 接口中添加一个 `getByIdAsync`方法来对其实现进行操作。代码应如下所示:

package org.acme.rest.client;

import java.util.Set;
import java.util.concurrent.CompletionStage;

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

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;

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

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

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

}

打开 `src/main/java/org/acme/rest/client/ExtensionsResource.java`文件并用以下内容更新它:

package org.acme.rest.client;

import java.util.Set;
import java.util.concurrent.CompletionStage;

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

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;

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

    @Inject
    @RestClient
    ExtensionsService extensionsService;

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

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

}

要测试异步方法,请在 `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 Classic"),
            "[0].keywords.size()", greaterThan(1),
            "[0].keywords", hasItem("rest-client"));
}

`Uni`版本非常相似:

package org.acme.rest.client;

import java.util.Set;
import java.util.concurrent.CompletionStage;

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

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;

import io.smallrye.mutiny.Uni;

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

    // ...

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

`ExtensionsResource`变为:

package org.acme.rest.client;

import java.util.Set;
import java.util.concurrent.CompletionStage;

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

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;

import io.smallrye.mutiny.Uni;

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

    @Inject
    @RestClient
    ExtensionsService extensionsService;


    // ...

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

前面的片段使用了 Mutiny reactive 类型。如果不熟悉 Mutiny,请参阅 Mutiny - an intuitive reactive programming library

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

@Inject @RestClient ExtensionsService extensionsService;

// ...

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

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

Custom headers support

MicroProfile REST 客户端允许通过使用具有 `@RegisterClientHeaders`注解的 `ClientHeadersFactory`来修改请求头。

让我们通过在 `ExtensionsService`REST 接口中添加一个指向 `RequestUUIDHeaderFactory`类的 `@RegisterClientHeaders`注解来对其进行操作:

package org.acme.rest.client;

import java.util.Set;
import java.util.concurrent.CompletionStage;

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

import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;

import io.smallrye.mutiny.Uni;

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

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

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

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

RequestUUIDHeaderFactory 将类似如下所示:

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。

Default header factory

你还可以使用没有指定任何自定义工厂的 @RegisterClientHeaders 声明。在这种情况下,将使用 DefaultClientHeadersFactoryImpl 工厂,并将修改 org.eclipse.microprofile.rest.client.propagateHeaders 配置属性中列出的所有头文件。各个头文件名称以逗号分隔。

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

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

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

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

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 对象,其中包含有关 REST 客户端扩展的一些基本信息。

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

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

REST Client and RESTEasy interactions

在 Quarkus 中,REST 客户端扩展和 the RESTEasy extension 共享相同的架构。这一考虑的一个重要后果是它们共享相同的提供程序列表(在 Jakarta REST 的术语意义上)。

例如,如果你声明一个 WriterInterceptor,它将默认拦截服务器调用和客户端调用,但这可能不是期望的行为。

但是,您可以更改此默认行为,并将一个提供程序约束为:

  • 仅通过向你的提供程序添加 @ConstrainedTo(RuntimeType.CLIENT) 声明来考虑 client 调用;

  • 仅通过向你的提供程序添加 @ConstrainedTo(RuntimeType.SERVER) 声明来考虑 server 调用。

Using a Mock HTTP Server for tests

在某些情况下,你可能希望模拟远程端点(HTTP 服务器),而不是模拟客户端本身。这对于原生测试或程序化创建的客户端可能特别有用。

你可以使用 Wiremock 轻松模拟 HTTP 服务器。Wiremock section of the Quarkus - Using the REST Client 详细介绍了如何设置该服务器。