Using the REST Client
本指南解释了如何使用 REST 客户端与 REST API 交互。REST 客户端与 Quarkus REST(之前的 RESTEasy Reactive)兼容的 REST 客户端实现。 如果您的应用程序使用客户端并公开 REST 终端节点,请对服务器部分使用Quarkus REST。
- Prerequisites
- Solution
- Creating the Maven project
- Setting up the model
- Create the interface
- Create the configuration
- Create the Jakarta REST resource
- Programmatic client creation with QuarkusRestClientBuilder
- Use Custom HTTP Options
- Redirection
- Update the test
- Async Support
- Custom headers support
- Customizing the request
- Customizing the ObjectMapper in REST Client Jackson
- Exception handling
- Multipart Form support
- Proxy support
- Package and run the application
- Logging traffic
- Mocking the client for tests
- Using a Mock HTTP Server for tests
- Known limitations
- Further reading
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
首先,我们需要一个新项目。使用以下命令创建一个新项目:
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 指南。
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`扩展添加到项目中:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下内容添加到构建文件中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
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 默认值,强烈建议使用 |
上述 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`将被添加到客户端的所有请求。因为我们使用了 `${…​} 语法,所以将使用 my.property-value 配置属性获取参数的实际值。 |
2 | 调用 getWithOtherParam 时,除了 my-param 查询参数外,还会添加值为 other 的 some-other-param 。 |
3 | 调用 getFromMethod 时,除了 my-param 查询参数外,还会添加值为 test 的 param-from-method (因为这是调用 param-from-method 时 withParam 方法返回的内容)。 |
请注意,如果接口方法包含用 @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);
}
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-client 的 ExtensionsService 的 getById 方法,将导致对 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);
}
}
|
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.<client-prefix>.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 Assured的 json-path功能。
Async Support
为了充分发挥客户端的反应式特性,你可以使用非阻塞风格的 REST Client 扩展,它支持 CompletionStage
和 Uni
。让我们通过在 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
中使用的通用类型。
用户还可以使用
|
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
方法,使用编程方式注册ReactiveClientHeadersFactory
或QuarkusRestClientBuilder.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 时,
其中 |
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 客户端通过过滤器支持进一步自定义发送到服务器的最终请求。过滤器必须实现接口 ClientRequestFilter
或 ResteasyReactiveClientRequestFilter
。
自定义请求的一个简单示例是添加自定义标头:
@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
会使用默认优先级,并且仍适用于依次调用所有处理器的正常规则。
使用 |
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);
指定为 File
、Path
、byte[]
、Buffer
或 FileUpload
的参数将作为文件发送,并且默认使用 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);
任何 @RestForm
的 File
、Path
、byte[]
、Buffer
或 FileUpload
类型的参数,以及任何使用 @PartType
进行注解的参数,在方法中都会自动暗示一个 @Consumes(MediaType.MULTIPART_FORM_DATA)
,前提是该方法中没有 @Consumes
。
如果存在不是暗示 multipart 的 |
有几种模式可以用来对表单数据进行编码。默认情况下,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 客户端使用 |
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();
目前,多部分响应支持受到以下限制:
-
多部分响应中发送的文件只能解析为
File
、Path`和 `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。
Proxy support
REST 客户端支持通过代理发送请求。它遵循其 JVM 设置,但也允许同时指定:
-
具有
quarkus.rest-client.proxy-address
、quarkus.rest-client.proxy-user
、quarkus.rest-client.proxy-password
、`quarkus.rest-client.non-proxy-hosts`的全局客户端代理设置 -
具有 `quarkus.rest-client.<my-client>.proxy-address`等每个客户端代理设置。这些设置仅应用于使用 CDI 注入的客户端,即使用 `@RegisterRestClient`创建的客户端
如果在客户端级别设置 proxy-address
,则客户端使用其特定代理设置。不会从全局配置或 JVM 属性传播任何代理设置。
如果未为客户端设置 `proxy-address`但在全局级别设置了它,则客户端使用全局设置。否则,客户端使用 JVM 设置。
设置代理的示例配置:
# global proxy configuration is used for all clients
quarkus.rest-client.proxy-address=localhost:8182
quarkus.rest-client.proxy-user=<proxy user name>
quarkus.rest-client.proxy-password=<proxy password>
quarkus.rest-client.non-proxy-hosts=example.com
# per-client configuration overrides the global settings for a specific client
quarkus.rest-client.my-client.proxy-address=localhost:8183
quarkus.rest-client.my-client.proxy-user=<proxy user name>
quarkus.rest-client.my-client.proxy-password=<proxy password>
quarkus.rest-client.my-client.url=...
MicroProfile REST 客户端规范不允许设置代理凭据。为了以编程方式指定代理用户和代理密码,你需要将 |
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
使用以下内容运行应用程序:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
使用 [role="bare"][role="bare"]http://localhost:8080/extension/id/io.quarkus:quarkus-rest-client 打开你的浏览器。
您应该会看到一个包含此扩展程序的一些基本信息 JSON 对象。
和往常一样,可以使用以下命令打包应用程序:
quarkus build
./mvnw install
./gradlew build
并且使用 java -jar target/quarkus-app/quarkus-run.jar
执行。
你还可以按如下方式生成本机可执行文件:
quarkus build --native
./mvnw install -Dnative
./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 客户端使用默认 |
Mocking the client for tests
如果您使用使用 @RestClient
注解注入的客户端,则可以轻松地对其进行模拟以进行测试。您可以使用 Mockito 的 @InjectMock
或 QuarkusMock
来执行此操作。
本节说明如何用模拟替换客户端。如果您希望更深入地了解 Quarkus 中的模拟工作方式,请参阅 Mocking CDI beans 上的博客文章。
在使用 |
我们假设您有以下客户端:
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
模拟测试客户端的最简单方法是。
首先,向你的应用添加以下依赖:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
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 服务器,针对它运行测试,这是常见的测试模式。此类服务器的示例包括 Wiremock 和 Hoverfly。在此部分我们将演示 Wiremock 如何用于测试在上面开发的 ExtensionsService
。
首先,需要将 Wiremock 添加为测试依赖。对于 Maven 项目,它会这样发生:
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
<version>${wiremock.version}</version> 1
</dependency>
1 | 使用正确的 Wiremock 版本。所有可用的版本都可在 here 找到。 |
testImplementation("org.wiremock:wiremock:$wiremockVersion") 1
1 | 使用正确的 Wiremock 版本。所有可用的版本都可在 here 找到。 |
在 Quarkus 测试中,当某些服务需要在 Quarkus 测试运行之前启动,我们使用 @io.quarkus.test.common.WithTestResource
注解指定一个 io.quarkus.test.common.QuarkusTestResourceLifecycleManager
,它能启动服务并提供 Quarkus 将使用的配置值。
有关 |
让我们像这样创建一个 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
属性设置为完全限定的作用域名称。 -
无法设置
HostnameVerifier
或SSLContext
-
一些对非阻塞实现没有意义的内容(比如设置
ExecutorService
)不起作用