Vert.x Web 客户端

Vert.x Web 客户端是一个异步 HTTP 和 HTTP/2 客户端。

Web 客户端使得与 Web 服务器进行 HTTP 请求/响应交互变得容易,并提供了以下高级功能:

  • Json 正文编码/解码

  • 请求/响应传输

  • 请求参数

  • 统一错误处理

  • 表单提交

Web 客户端并没有废弃 Vert.x Core 的 HttpClient,事实上它就是基于这个客户端的,并继承了其配置和诸如连接池、HTTP/2 支持、流水线支持等强大功能。当需要对 HTTP 请求/响应进行精细控制时,应使用 HttpClient

Web 客户端不提供 WebSocket API,应使用 Vert.x Core 的 HttpClient。它目前也不处理 cookie。

使用 Web 客户端

要使用 Vert.x Web 客户端,请将以下依赖项添加到您的构建描述符的 dependencies 部分中:

  • Maven(在您的 pom.xml 中)

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-web-client</artifactId>
  <version>5.0.1</version>
</dependency>
  • Gradle(在您的 build.gradle 文件中)

dependencies {
  compile 'io.vertx:vertx-web-client:5.0.1'
}

Vert.x 核心 HTTP 客户端回顾

Vert.x Web 客户端使用了 Vert.x 核心的 API,因此,如果您还不熟悉 Vert.x 核心中 HttpClient 的基本概念,那么熟悉一下这些概念将非常有价值。

创建 Web 客户端

您可以按如下方式创建具有默认选项的 WebClient 实例:

WebClient client = WebClient.create(vertx);

如果您想为客户端配置选项,您可以按如下方式创建它:

WebClientOptions options = new WebClientOptions()
  .setUserAgent("My-App/1.2.3");
options.setKeepAlive(false);
WebClient client = WebClient.create(vertx, options);

Web 客户端选项继承了 Http 客户端选项,因此您可以设置其中任何一个。

如果您的应用程序中已经有一个 HTTP 客户端,您也可以重复使用它:

WebClient client = WebClient.wrap(httpClient);
在大多数情况下,Web 客户端应在应用程序启动时创建一次,然后重复使用。否则,您将失去许多好处,例如连接池,并且如果实例未正确关闭,可能会导致资源泄漏。

发起请求

无请求体的简单请求

通常,您会希望发起不带请求体的 HTTP 请求。这通常适用于 HTTP GET、OPTIONS 和 HEAD 请求:

WebClient client = WebClient.create(vertx);

// Send a GET request
client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .onSuccess(response -> System.out
    .println("Received response with status code" + response.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

// Send a HEAD request
client
  .head(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .onSuccess(response -> System.out
    .println("Received response with status code" + response.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

您可以流畅地向请求 URI 添加查询参数:

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .addQueryParam("param", "param_value")
  .send()
  .onSuccess(response -> System.out
    .println("Received response with status code" + response.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

任何请求 URI 参数都将预填充请求:

HttpRequest<Buffer> request = client
  .get(
    8080,
    "myserver.mycompany.com",
    "/some-uri?param1=param1_value&param2=param2_value");

// Add param3
request.addQueryParam("param3", "param3_value");

// Overwrite param2
request.setQueryParam("param2", "another_param2_value");

设置请求 URI 会丢弃现有查询参数:

HttpRequest<Buffer> request = client
  .get(8080, "myserver.mycompany.com", "/some-uri");

// Add param1
request.addQueryParam("param1", "param1_value");

// Overwrite param1 and add param2
request.uri("/some-uri?param1=param1_value&param2=param2_value");

写入请求体

当您需要发起带请求体的请求时,您可以使用相同的 API,然后调用期望发送请求体的 sendXXX 方法。

使用 sendBuffer 发送缓冲区正文

client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendBuffer(buffer)
  .onSuccess(res -> {
    // OK
  });

发送单个缓冲区很有用,但通常您不希望将内容完全加载到内存中,因为它可能太大,或者您希望处理许多并发请求并希望每个请求仅使用最小的内存。为此,Web 客户端可以使用 sendStream 方法发送 ReadStream<Buffer>(例如 AsyncFileReadStream<Buffer>):

client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendStream(stream)
  .onSuccess(res -> {
    // OK
  });

Web 客户端会为您处理传输泵的设置。由于流的长度未知,请求将使用分块传输编码。

当您知道流的大小后,应在使用 content-length 标头之前指定:

fs.open("content.txt", new OpenOptions())
  .onSuccess(fileStream -> {
    String fileLen = "1024";

    // Send the file to the server using POST
    client
      .post(8080, "myserver.mycompany.com", "/some-uri")
      .putHeader("content-length", fileLen)
      .sendStream(fileStream)
      .onSuccess(res -> {
        // OK
      });
});

POST 请求将不会分块。

Json 正文

通常您会希望发送 Json 正文请求,要发送 JsonObject 请使用 sendJsonObject

client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendJsonObject(
    new JsonObject()
      .put("firstName", "Dale")
      .put("lastName", "Cooper"))
  .onSuccess(res -> {
    // OK
  });

在 Java、Groovy 或 Kotlin 中,您可以使用 sendJson 方法,该方法通过 Json.encode 方法将 POJO (Plain Old Java Object) 映射到 Json 对象:

client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendJson(new User("Dale", "Cooper"))
  .onSuccess(res -> {
    // OK
  });
Json.encode 使用 Jackson 映射器将对象编码为 Json。

表单提交

您可以使用 sendForm 变体发送 http 表单提交正文。

MultiMap form = MultiMap.caseInsensitiveMultiMap();
form.set("firstName", "Dale");
form.set("lastName", "Cooper");

// Submit the form as a form URL encoded body
client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendForm(form)
  .onSuccess(res -> {
    // OK
  });

默认情况下,表单以 application/x-www-form-urlencoded 内容类型标头提交。您可以将 content-type 标头设置为 multipart/form-data

MultiMap form = MultiMap.caseInsensitiveMultiMap();
form.set("firstName", "Dale");
form.set("lastName", "Cooper");

// Submit the form as a multipart form body
client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .putHeader("content-type", "multipart/form-data")
  .sendForm(form)
  .onSuccess(res -> {
    // OK
  });

如果您想上传文件和发送属性,您可以创建一个 MultipartForm 并使用 sendMultipartForm

MultipartForm form = MultipartForm.create()
  .attribute("imageDescription", "a very nice image")
  .binaryFileUpload(
    "imageFile",
    "image.jpg",
    "/path/to/image",
    "image/jpeg");

// Submit the form as a multipart form body
client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendMultipartForm(form)
  .onSuccess(res -> {
    // OK
  });

写入请求头

您可以使用如下的 headers multi-map 向请求写入头:

HttpRequest<Buffer> request = client
  .get(8080, "myserver.mycompany.com", "/some-uri");

MultiMap headers = request.headers();
headers.set("content-type", "application/json");
headers.set("other-header", "foo");

头是 MultiMap 的一个实例,它提供了添加、设置和删除条目的操作。Http 头允许一个特定键对应多个值。

您也可以使用 putHeader 写入头:

HttpRequest<Buffer> request = client
  .get(8080, "myserver.mycompany.com", "/some-uri");

request.putHeader("content-type", "application/json");
request.putHeader("other-header", "foo");

配置请求以添加身份验证。

身份验证可以通过手动设置正确的头来执行,或者使用我们预定义的方法(我们强烈建议启用 HTTPS,特别是对于需要身份验证的请求):

在基本 HTTP 身份验证中,请求包含一个 Authorization: Basic <credentials> 形式的头字段,其中 credentials 是由冒号连接的 id 和密码的 base64 编码。

您可以按如下方式配置请求以添加基本访问身份验证:

HttpRequest<Buffer> request = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .authentication(new UsernamePasswordCredentials("myid", "mypassword"));

在 OAuth 2.0 中,请求包含一个 Authorization: Bearer <bearerToken> 形式的头字段,其中 bearerToken 是由授权服务器颁发用于访问受保护资源的持有者令牌。

您可以按如下方式配置请求以添加持有者令牌身份验证:

HttpRequest<Buffer> request = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .authentication(new TokenCredentials("myBearerToken"));

请求复用

send 方法可以安全地多次调用,这使得配置和复用 HttpRequest 对象变得非常容易:

HttpRequest<Buffer> get = client
  .get(8080, "myserver.mycompany.com", "/some-uri");

get
  .send()
  .onSuccess(res -> {
    // OK
  });

// Same request again
get
  .send()
  .onSuccess(res -> {
    // OK
  });

但请注意,HttpRequest 实例是可变的。因此,在修改缓存的实例之前,您应该调用 copy 方法。

HttpRequest<Buffer> get = client
  .get(8080, "myserver.mycompany.com", "/some-uri");

get
  .send()
  .onSuccess(res -> {
    // OK
  });

// The "get" request instance remains unmodified
get
  .copy()
  .putHeader("a-header", "with-some-value")
  .send()
  .onSuccess(res -> {
    // OK
  });

超时

您可以使用 connectTimeout 为特定的 http 请求设置连接超时。

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .connectTimeout(5000)
  .send()
  .onSuccess(res -> {
    // OK
  })
  .onFailure(err -> {
    // Might be a timeout when cause is java.util.concurrent.TimeoutException
  });

如果客户端未能在超时期限内获取到与服务器的连接,则会将异常传递给响应处理程序。

您可以使用 idleTimeout 为特定的 http 请求设置空闲超时。

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .idleTimeout(5000)
  .send()
  .onSuccess(res -> {
    // OK
  })
  .onFailure(err -> {
    // Might be a timeout when cause is java.util.concurrent.TimeoutException
  });

如果在超时期限内请求未返回任何数据,则会将异常传递给响应处理程序。

您可以使用 timeout 设置两种超时

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .timeout(5000)
  .send()
  .onSuccess(res -> {
    // OK
  })
  .onFailure(err -> {
    // Might be a timeout when cause is java.util.concurrent.TimeoutException
  });

处理 HTTP 响应

当 Web 客户端发送请求时,您总是处理单个异步结果 HttpResponse

成功结果的回调会在收到响应后发生

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .onSuccess(res ->
    System.out.println("Received response with status code" + res.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

默认情况下,Vert.x Web 客户端请求仅在网络层面发生错误时才以错误结束。换句话说,404 Not Found 响应或内容类型错误的响应**不**被视为失败。如果您希望 Web 客户端自动执行健全性检查,请使用 http 响应期望

响应是完全缓冲的,使用 BodyCodec.pipe 将响应传输到写入流

解码响应

默认情况下,Web 客户端将 http 响应体作为 Buffer 提供,并且不应用任何解码。

可以使用 BodyCodec 实现自定义响应体解码

  • 纯字符串

  • Json 对象

  • Json 映射的 POJO

  • 写入流

正文编解码器可以将任意二进制数据流解码为特定的对象实例,从而省去了响应处理程序中的解码步骤。

使用 BodyCodec.jsonObject 解码 Json 对象

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .as(BodyCodec.jsonObject())
  .send()
  .onSuccess(res -> {
    JsonObject body = res.body();

    System.out.println(
      "Received response with status code" +
        res.statusCode() +
        " with body " +
        body);
  })
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

在 Java、Groovy 或 Kotlin 中,可以解码自定义的 Json 映射 POJO

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .as(BodyCodec.json(User.class))
  .send()
  .onSuccess(res -> {
    User user = res.body();

    System.out.println(
      "Received response with status code" +
        res.statusCode() +
        " with body " +
        user.getFirstName() +
        " " +
        user.getLastName());
  })
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

当预期有大型响应时,使用 BodyCodec.pipe。此正文编解码器将响应正文缓冲区传输到 WriteStream,并在异步结果响应中指示操作的成功或失败。

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .as(BodyCodec.pipe(writeStream))
  .send()
  .onSuccess(res ->
    System.out.println("Received response with status code" + res.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

API 返回 JSON 对象流的情况变得很常见。例如,Twitter API 可以提供推文(tweets)的提要(feed)。要处理此用例,您可以使用 BodyCodec.jsonStream。您传入一个 JSON 解析器,它从 HTTP 响应中发出读取到的 JSON 流。

JsonParser parser = JsonParser.newParser().objectValueMode();
parser.handler(event -> {
  JsonObject object = event.objectValue();
  System.out.println("Got " + object.encode());
});
client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .as(BodyCodec.jsonStream(parser))
  .send()
  .onSuccess(res ->
    System.out.println("Received response with status code" + res.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

最后,如果您对响应内容完全不感兴趣,BodyCodec.none 会简单地丢弃整个响应体。

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .as(BodyCodec.none())
  .send()
  .onSuccess(res ->
    System.out.println("Received response with status code" + res.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

当您事先不知道 http 响应的内容类型时,您仍然可以使用 bodyAsXXX() 方法将响应解码为特定类型

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .onSuccess(res -> {
    // Decode the body as a json object
    JsonObject body = res.bodyAsJsonObject();

    System.out.println(
      "Received response with status code" +
        res.statusCode() +
        " with body " +
        body);
  })
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));
这仅对解码为缓冲区的响应有效。

响应期望

默认情况下,Vert.x Web 客户端请求仅在网络层面发生错误时才以错误结束。

换句话说,您必须在收到响应后手动执行健全性检查

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .onSuccess(res -> {
    if (
      res.statusCode() == 200 &&
        res.getHeader("content-type").equals("application/json")) {
      // Decode the body as a json object
      JsonObject body = res.bodyAsJsonObject();

      System.out.println(
        "Received response with status code" +
          res.statusCode() +
          " with body " +
          body);
    } else {
      System.out.println("Something went wrong " + res.statusCode());
    }
  })
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

您可以使用*响应期望*来权衡灵活性、清晰度和简洁性。

Response expectations 可以在响应不符合某个条件时使请求失败。

Web 客户端可以重用 Vert.x HTTP 客户端预定义的期望

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .expecting(HttpResponseExpectation.SC_SUCCESS.and(HttpResponseExpectation.JSON))
  .onSuccess(res -> {
    // Safely decode the body as a json object
    JsonObject body = res.bodyAsJsonObject();
    System.out.println(
      "Received response with status code" +
        res.statusCode() +
        " with body " +
        body);
  })
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

当现有期望不符合您的需求时,您也可以创建自定义期望

Expectation<HttpResponseHead> methodsPredicate = new Expectation<HttpResponseHead>() {
  @Override
  public boolean test(HttpResponseHead resp) {
    String methods = resp.getHeader("Access-Control-Allow-Methods");
    return methods != null && methods.contains("POST");
  }
};

// Send pre-flight CORS request
client
  .request(
    HttpMethod.OPTIONS,
    8080,
    "myserver.mycompany.com",
    "/some-uri")
  .putHeader("Origin", "Server-b.com")
  .putHeader("Access-Control-Request-Method", "POST")
  .send()
  .expecting(methodsPredicate)
  .onSuccess(res -> {
    // Process the POST request now
  })
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

预定义期望

为方便起见,Vert.x HTTP 客户端附带了一些适用于常见用例的期望,这些期望也适用于 Web 客户端。

对于状态码,例如 HttpResponseExpectation.SC_SUCCESS 用于验证响应是否具有 2xx 代码,您也可以创建自定义的期望

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .expecting(HttpResponseExpectation.status(200, 202))
  .onSuccess(res -> {
    // ....
  });

对于内容类型,例如 HttpResponseExpectation.JSON 用于验证响应体是否包含 JSON 数据,您也可以创建自定义的期望

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .expecting(HttpResponseExpectation.contentType("some/content-type"))
  .onSuccess(res -> {
    // ....
  });

请参阅 HttpResponseExpectation 文档以获取预定义谓词的完整列表。

创建自定义失败

默认情况下,响应期望(包括预定义的期望)使用一个默认的错误转换器,该转换器会丢弃正文并传递一个简单消息。您可以通过映射失败来自定义异常类

Expectation<HttpResponseHead> expectation = HttpResponseExpectation.SC_SUCCESS
  .wrappingFailure((resp, err) -> new MyCustomException(err.getMessage()));

许多 Web API 在错误响应中提供详细信息。例如,[Marvel API](https://developer.marvel.com/docs) 使用此 JSON 对象格式

{
  "code": "InvalidCredentials",
  "message": "The passed API key is invalid."
}

为了避免丢失此信息,可以转换响应体

HttpResponseExpectation.SC_SUCCESS.wrappingFailure((resp, err) -> {
  // Invoked after the response body is fully received
  HttpResponse<?> response =(HttpResponse<?>) resp;

  if (response
    .getHeader("content-type")
    .equals("application/json")) {

    // Error body is JSON data
    JsonObject body = response.bodyAsJsonObject();

    return new MyCustomException(
      body.getString("code"),
      body.getString("message"));
  }

  // Fallback to defaut message
  return new MyCustomException(err.getMessage());
});
在 Java 中创建异常在捕获堆栈跟踪时可能会产生性能开销,因此您可能希望创建不捕获堆栈跟踪的异常。默认情况下,异常是使用不捕获堆栈跟踪的异常报告的。

处理 30x 重定向

默认情况下,客户端会遵循重定向,您可以在 WebClientOptions 中配置默认行为

WebClient client = WebClient
  .create(vertx, new WebClientOptions().setFollowRedirects(false));

客户端最多会遵循 16 个请求重定向,这可以在相同的选项中更改

WebClient client = WebClient
  .create(vertx, new WebClientOptions().setMaxRedirects(5));
出于安全原因,客户端不会遵循除 GET 或 HEAD 之外的方法请求的重定向

客户端负载均衡

默认情况下,当客户端将主机名解析为多个 IP 地址列表时,客户端会使用返回的第一个 IP 地址。

客户端可以配置为执行客户端负载均衡

WebClient client = WebClient.wrap(vertx
  .httpClientBuilder()
  .withLoadBalancer(LoadBalancer.ROUND_ROBIN)
  .build());

Vert.x 开箱即用地提供了几种您可以使用的负载均衡策略

大多数负载均衡策略都是不言自明的。

基于哈希的路由可以通过 LoadBalancer.CONSISTENT_HASHING 策略实现。

WebClient client = WebClient.wrap(vertx
  .httpClientBuilder()
  .withLoadBalancer(LoadBalancer.ROUND_ROBIN)
  .build());

您可以在 Vert.x Core HTTP 客户端文档中阅读有关客户端负载均衡的更多详细信息。

HTTP 响应缓存

Vert.x Web 提供了一个 HTTP 响应缓存功能;要使用它,您需要创建一个 CachingWebClient

创建缓存 Web 客户端

WebClient client = WebClient.create(vertx);
WebClient cachingWebClient = CachingWebClient.create(client);

配置缓存内容

默认情况下,缓存 Web 客户端将仅缓存 GET 方法中状态码为 200301404 的响应。此外,默认情况下,包含 Vary 头的响应将不会被缓存。

这可以通过在客户端创建时传递 CachingWebClientOptions 来配置。

CachingWebClientOptions options = new CachingWebClientOptions()
  .addCachedMethod(HttpMethod.HEAD)
  .removeCachedStatusCode(301)
  .setEnableVaryCaching(true);

WebClient client = WebClient.create(vertx);
WebClient cachingWebClient = CachingWebClient.create(client, options);

除非客户端也是 WebClientSession,否则 Cache-Control 头中包含 private 指令的响应将不会被缓存。请参阅处理私有响应

提供外部存储

在存储响应时,默认的缓存客户端将使用本地 Map。您可以提供自己的存储实现来存储响应。为此,请实现 CacheStore,然后在创建客户端时提供它。

WebClient client = WebClient.create(vertx);
CacheStore store = new NoOpCacheStore(); // or any store you like
WebClient cachingWebClient = CachingWebClient.create(client, store);

处理私有响应

要启用私有响应缓存,CachingWebClient 可以与 WebClientSession 结合使用。这样做后,公共响应(即 Cache-Control 头中带有 public 指令的响应)将缓存在创建客户端时所用的 CacheStore 中。私有响应(即 Cache-Control 头中带有 private 指令的响应)将与会话一起缓存,以确保缓存的响应不会泄露给其他用户(会话)。

要创建一个可以缓存私有响应的客户端,请将 CachingWebClient 传递给 WebClientSession

WebClient client = WebClient.create(vertx);
WebClient cachingWebClient = CachingWebClient.create(client);
WebClient sessionClient = WebClientSession.create(cachingWebClient);

URI 模板

URI 模板提供了一种基于 [URI 模板 RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) 的 HTTP 请求字符串 URI 的替代方案。

您可以阅读 Vert.x URI 模板[文档](../../vertx-uri-template/java/)以了解更多信息。

您可以使用 UriTemplate URI 而不是 Java 字符串 URI 来创建 HttpRequest

首先将模板字符串解析为 UriTemplate

UriTemplate REQUEST_URI = UriTemplate.of("/some-uri?{param}");

然后使用它来创建请求

HttpRequest<Buffer> request = client.get(8080, "myserver.mycompany.com", REQUEST_URI);

设置模板参数

request.setTemplateParam("param", "param_value");

最后像往常一样发送请求

request.send()
  .onSuccess(res ->
    System.out.println("Received response with status code" + res.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

或流畅地

client.get(8080, "myserver.mycompany.com", REQUEST_URI)
  .setTemplateParam("param", "param_value")
  .send()
  .onSuccess(res ->
    System.out.println("Received response with status code" + res.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

URI 模板扩展

在发送请求之前,Vert.x WebClient 会使用请求模板参数将模板扩展为字符串。

字符串扩展会为您处理参数编码,

String euroSymbol = "\u20AC";
UriTemplate template = UriTemplate.of("/convert?{amount}&{currency}");

// Request uri: /convert?amount=1234&currency=%E2%82%AC
Future<HttpResponse<Buffer>> fut = client.get(template)
  .setTemplateParam("amount", amount)
  .setTemplateParam("currency", euroSymbol)
  .send();

默认的扩展语法称为*简单字符串扩展*,还有其他扩展语法可用

  • *路径段扩展* ({/varname})

  • *表单式查询扩展* ({?varname})

  • 等等…​

您可以参考 Vert.x URI 模板文档(可用时添加链接)以全面了解各种扩展样式。

根据 RFC 的要求,模板扩展会将缺失的模板参数替换为空字符串。您可以将此行为更改为失败:

WebClient webClient = WebClient.create(vertx, new WebClientOptions()
  .setTemplateExpandOptions(new ExpandOptions()
    .setAllowVariableMiss(false))
);

模板参数值

模板参数接受 StringList<String>Map<String, String> 值。

每种类型的扩展都取决于扩展样式(由 ? 前缀表示),这里是一个 *query* 参数的示例,它被展开(由 * 后缀表示)并使用表单式查询扩展进行扩展:

Map<String, String> query = new HashMap<>();
query.put("color", "red");
query.put("width", "30");
query.put("height", "50");
UriTemplate template = UriTemplate.of("/{?query*}");

// Request uri: /?color=red&width=30&height=50
Future<HttpResponse<Buffer>> fut = client.getAbs(template)
  .setTemplateParam("query", query)
  .send();

根据定义,表单式查询扩展将变量 {?query*} 扩展为 ?color=red&width=30&height=50

使用 HTTPS

Vert.x Web 客户端可以按照与 Vert.x HttpClient 完全相同的方式配置为使用 HTTPS。

您可以为每个请求指定行为

client
  .get(443, "myserver.mycompany.com", "/some-uri")
  .ssl(true)
  .send()
  .onSuccess(res ->
    System.out.println("Received response with status code" + res.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

或使用带绝对 URI 参数的创建方法

client
  .getAbs("https://myserver.mycompany.com:4043/some-uri")
  .send()
  .onSuccess(res ->
    System.out.println("Received response with status code" + res.statusCode()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));

会话管理

Vert.x Web 提供了一个 Web 会话管理功能;要使用它,您需要为每个用户(会话)创建一个 WebClientSession,并使用它而不是 WebClient

创建 WebClientSession

您可以按如下方式创建 WebClientSession 实例:

WebClient client = WebClient.create(vertx);
WebClientSession session = WebClientSession.create(client);

发起请求

创建后,WebClientSession 可以用来代替 WebClient 发起 HTTP(s) 请求,并自动管理从您调用的服务器接收到的任何 cookie。

设置会话级别请求头

您可以按如下方式设置要添加到每个请求的会话级别请求头:

WebClientSession session = WebClientSession.create(client);
session.addHeader("my-jwt-token", jwtToken);

这些请求头将被添加到每个请求中;请注意,这些请求头将发送到所有主机;如果您需要向不同主机发送不同的请求头,则必须手动将它们添加到每个请求中,而不是添加到 WebClientSession

OAuth2 安全

Vert.x Web 提供了一个 Web 会话管理功能;要使用它,您需要为每个用户(会话)创建一个 OAuth2WebClient,并使用它而不是 WebClient

创建 OAuth2 客户端

您可以按如下方式创建 OAuth2WebClient 实例:

WebClient client = WebClient.create(vertx);
OAuth2WebClient oauth2 = OAuth2WebClient.create(
    client,
    OAuth2Auth.create(vertx, new OAuth2Options(/* enter IdP config */)))

  // configure the initial credentials (needed to fetch if needed
  // the access_token
  .withCredentials(new TokenCredentials("some.jwt.token"));

客户端还可以利用 OpenId 服务发现来完全配置客户端,例如连接到真实的 keycloak 服务器只需执行:

KeycloakAuth.discover(
    vertx,
    new OAuth2Options().setSite("https://keycloakserver.com"))
  .onSuccess(oauth -> {
    OAuth2WebClient client = OAuth2WebClient.create(
        WebClient.create(vertx),
        oauth)
      // if your keycloak is configured for password_credentials_flow
      .withCredentials(
        new UsernamePasswordCredentials("bob", "s3cret"));
  });

发起请求

创建后,OAuth2WebClient 可以用来代替 WebClient 发起 HTTP(s) 请求,并自动管理从您调用的服务器接收到的任何 cookie。

避免令牌过期

您可以按如下方式为每个请求设置令牌过期宽限期:

OAuth2WebClient client = OAuth2WebClient.create(
    baseClient,
    oAuth2Auth,
    new OAuth2WebClientOptions()
      .setLeeway(5));

如果需要执行请求,会检查当前活动用户对象的过期状态并附加给定的宽限期。这将允许客户端在需要时执行令牌刷新,而不是因错误中止操作。

请求仍可能由于令牌过期而失败,因为过期计算仍将在服务器端执行。为了减少用户端的工作,客户端可以配置为对返回状态码 401(禁止)的请求执行**单次**重试。当选项标志:refreshTokenOnForbidden 设置为 true 时,客户端将执行新的令牌请求,并在将响应传递给用户处理程序/Promise 之前重试原始请求。

OAuth2WebClient client = OAuth2WebClient.create(
  baseClient,
  oAuth2Auth,
  new OAuth2WebClientOptions()
    // the client will attempt a single token request, if the request
    // if the status code of the response is 401
    // there will be only 1 attempt, so the second consecutive 401
    // will be passed down to your handler/promise
    .setRenewTokenOnForbidden(true));

RxJava 3 API

RxJava 的 HttpRequest 提供了原始 API 的 Rx 封装版本,rxSend 方法返回一个 Single<HttpResponse<Buffer>>,该 Single 在订阅时发起 HTTP 请求,因此可以多次订阅。

Single<HttpResponse<Buffer>> single = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .rxSend();

// Send a request upon subscription of the Single
single.subscribe(response -> System.out.println("Received 1st response with status code" + response.statusCode()), error -> System.out.println("Something went wrong " + error.getMessage()));

// Send another request
single.subscribe(response -> System.out.println("Received 2nd response with status code" + response.statusCode()), error -> System.out.println("Something went wrong " + error.getMessage()));

获得的 Single 可以与 RxJava API 自然地组合和链式调用

Single<String> url = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .rxSend()
  .map(HttpResponse::bodyAsString);

// Use the flatMap operator to make a request on the URL Single
url
  .flatMap(u -> client.getAbs(u).rxSend())
  .subscribe(response -> System.out.println("Received response with status code" + response.statusCode()), error -> System.out.println("Something went wrong " + error.getMessage()));

相同的 API 可用

Single<HttpResponse<JsonObject>> single = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .putHeader("some-header", "header-value")
  .addQueryParam("some-param", "param value")
  .as(BodyCodec.jsonObject())
  .rxSend();
single.subscribe(resp -> {
  System.out.println(resp.statusCode());
  System.out.println(resp.body());
});

发送 Flowable<Buffer> 正文时,应优先使用 rxSendStream

Flowable<Buffer> body = getPayload();

Single<HttpResponse<Buffer>> single = client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .rxSendStream(body);
single.subscribe(resp -> {
  System.out.println(resp.statusCode());
  System.out.println(resp.body());
});

订阅后,将订阅 body 并将其内容用于请求。

HTTP 响应期望

与 HTTP 后端交互通常涉及验证 HTTP 响应代码和/或内容类型。

为了简化验证过程,请使用 HttpResponseExpectation 方法

Single<HttpResponse<Buffer>> single = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .rxSend()
  // Transforms the single into a failed single if the HTTP response is not successful
  .compose(HttpResponseExpectation.status(200))
  // Transforms the single into a failed single if the HTTP response content is not JSON
  .compose(HttpResponseExpectation.contentType("application/json"));

Unix 域套接字

Web 客户端支持 Unix 域套接字。例如,您可以与[本地 Docker 守护程序](https://docs.dockerd.com.cn/engine/reference/commandline/dockerd/)进行交互。

要实现这一点,您必须使用 JDK16+ 运行您的应用程序,或者使用[原生传输](../../vertx-core/java/#_native_transports)创建 Vertx 实例。

SocketAddress serverAddress = SocketAddress
  .domainSocketAddress("/var/run/docker.sock");

// We still need to specify host and port so the request
// HTTP header will be localhost:8080
// otherwise it will be a malformed HTTP request
// the actual value does not matter much for this example
client
  .request(
    HttpMethod.GET,
    serverAddress,
    8080,
    "localhost",
    "/images/json")
  .as(BodyCodec.jsonObject())
  .send()
  .expecting(HttpResponseExpectation.SC_ACCEPTED)
  .onSuccess(res ->
    System.out.println("Current Docker images" + res.body()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));