Vert.x Web Client

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

Web Client使得发送 HTTP 请求以及从 Web 服务器接收 HTTP 响应变得更加便捷,同时提供了额外的高级功能, 例如:

  • Json body的编码和解码

  • 请求和响应泵

  • 请求参数的处理

  • 统一的错误处理

  • 提交表单

制作Web Client的目的并非为了替换Vert.x Core中的 HttpClient ,而是基于该客户端, 继承其配置和强大的功能,例如请求连接池(Pooling),HTTP/2的支持,流水线/管线的支持等。 当您需要对 HTTP 请求和响应做细微粒度控制时, 您应当使用 HttpClient

另外Web Client并未提供 WebSocket API,此时您应当使用 Vert.x Core的 HttpClient 。 目前还无法处理cookies。

使用Web Client

如需使用Vert.x Web Client,请先加入以下依赖,到您的build描述 dependencies 部分 :

  • Maven (在您的 pom.xml 文件内):

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

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

回顾 Vert.x Core的 HTTP Client

Vert.x Web Client使用Vert.x core的API,如果您对此还不熟悉,熟悉基于 Vert.x core的 HttpClient 基本概念是很有价值的。

创建Web Client

您可以创建一个缺省 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 Client配置项继承自 HttpClient 配置项,您可以设置其中任何一个项。

如果已在程序中创建 HttpClient,可用以下方式复用:

WebClient client = WebClient.wrap(httpClient);
重要
在大多数情况下,一个Web Client 应该在应用程序启动时创建,并重用它。 否则,您将失去很多好处,例如连接池。如果实例未正确关闭,则可能会资源泄漏。

发送请求

无请求体的简单请求

通常,您想发送一个无请求体的HTTP请求。以下是一般情况下的 HTTP GET, OPTIONS和HEAD 请求

WebClient client = WebClient.create(vertx);

// 发送GET请求
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()));

// 发送HEAD请求
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");

// 添加 param3
request.addQueryParam("param3", "param3_value");

// 覆盖 param2
request.setQueryParam("param2", "another_param2_value");

设置请求URI将会自动清除已有的查询参数

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

// 添加 param1
request.addQueryParam("param1", "param1_value");

// 覆盖 param1 并添加 param2
request.uri("/some-uri?param1=param1_value&param2=param2_value");

填充请求体

如需要发送请求体,可使用相同的API,并在最后加上 sendXXX 方法, 发送相应的请求体。

使用 sendBuffer 发送一个buffer body

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

发送single buffer很有用,但是通常您不想完全将内容加载到内存中, 因为它可能太大,或者您想同时处理多个请求,或者每个请求只想使用最小的(消耗)。 为此,Web Client可以使用 ReadStream<Buffer> 的(例如 AsyncFile 是一个ReadStream<Buffer>) sendStream 方法发送。

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

Web Client负责为您设置泵传输(transfer pump)。 如果流长度未知则使用分块传输(chunked transfer)编码。

当您知道流的大小,您应该在HTTP header中设置 content-length

fs.open("content.txt", new OpenOptions(), fileRes -> {
  if (fileRes.succeeded()) {
    ReadStream<Buffer> fileStream = fileRes.result();

    String fileLen = "1024";

    // 用POST方法发送文件
    client
      .post(8080, "myserver.mycompany.com", "/some-uri")
      .putHeader("content-length", fileLen)
      .sendStream(fileStream)
      .onSuccess(res -> {
        // OK
      })
    ;
  }
});

这个POST方法不会被分块传输。

JSON bodies

有时您需要发送JSON body请求, 可使用 sendJsonObject 发送一个 JsonObject

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 mapper将 POJO 编码成 JSON。

表单提交

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

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

// 用URL编码方式提交表单
client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendForm(form)
  .onSuccess(res -> {
    // OK
  });

默认情况下,提交表单header中的 content-type 属性值为 application/x-www-form-urlencoded, 您还可将其替换为 multipart/form-data

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

// 提交multipart form表单
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");

// 提交multipart form表单
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");

此处 Headers 是一个 MultiMap 实例,提供了添加、 设置以及删除头属性操作的入口。HTTP headers允许某个特定的key有多个值。

您还可使用 putHeader 写入headers属性:

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

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

配置请求以添加身份验证

可以通过设置正确的 headers 来手动执行身份验证,或我们的预定义方法 (我们强烈建议启用HTTPS,尤其是对于经过身份验证的请求):

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

您可以像下面这样配置请求以添加基本访问验证:

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

在OAuth 2.0种,请求包含以下形式的表单header字段 Authorization: Bearer <bearerToken> ,bearerToken是授权服务器发布的, 用于访问受保护资源的不记名令牌。

您可以像下面这样配置请求,以添加bearer token访问验证:

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

// 又一些请求
get
  .send()
  .onSuccess(res -> {
    // OK
  });

不过要当心 HttpRequest 实例是可变的(mutable). 因此,您应该在修改已被缓存了的实例之前,使用 copy 方法。

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

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

// "get" 请求实例保持未修改
get
  .copy()
  .putHeader("a-header", "with-some-value")
  .send()
  .onSuccess(res -> {
    // OK
  });

超时

您可通过 timeout 。方法设置超时时间。

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .timeout(5000)
  .send()
  .onSuccess(res -> {
    // OK
  })
  .onFailure(err -> {
    // 当是由java.util.concurrent.TimeoutException导致时,或许是一个超时
  });

若请求在设定时间内没有返回任何数据, 则一个异常将会传递给响应处理器。

处理HTTP响应

Web Client 请求发送之后,您总是在单个 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 Client 请求才以错误结尾。 换言之,404 Not Found 响应或错误 content type 的响应, 被视为失败。 如果您想要 Web Client 自动进行合理性检,可以使用 响应谓词

警告
响应会被完全缓冲,请使用 BodyCodec.pipe 将响应接入写入流。

响应解码

缺省状况下,Web Client提供一个response body作为 Buffer , 并且未运用任何解码器。

可以使用 BodyCodec 实现以下自定义response body解码:

  • 文本字符串

  • Json 对象

  • Json 映射的 POJO

  • WriteStream

一个body解码器可以将任意二进制数据流解码为特定的对象实例, 从而节省了您自己在响应处理器里解码的步骤。

使用 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可以提供一个推文回馈。 处理这个情况,您可以使用 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()));

若无法预知响应内容类型,您依旧可以在获取结果之后,用 bodyAsXXX() 方法将其转换成指定的类型

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .onSuccess(res -> {
    // 将结果解码为Json对象
    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()));
警告
这种方式仅对响应结果为buffer有效。

响应谓词

默认的,仅当在网络级别发生错误时,Vert.x Web Client请求才以错误结尾。

换言之, 您必须在收到响应后手动执行健全性检查:

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .onSuccess(res -> {
    if (
      res.statusCode() == 200 &&
        res.getHeader("content-type").equals("application/json")) {
      // 将结果解码为Json对象
      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 predicates

Response predicates 当响应不符合条件会使请求失败。

Web Client附带了一组开箱即用的谓词,可供使用:

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .expect(ResponsePredicate.SC_SUCCESS)
  .expect(ResponsePredicate.JSON)
  .send()
  .onSuccess(res -> {
    // 安全地将body解码为json对象
    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()));

当现有谓词不满足您的需求时,您还可以创建自定义谓词:

Function<HttpResponse<Void>, ResponsePredicateResult> methodsPredicate =
  resp -> {
    String methods = resp.getHeader("Access-Control-Allow-Methods");
    if (methods != null) {
      if (methods.contains("POST")) {
        return ResponsePredicateResult.success();
      }
    }
    return ResponsePredicateResult.failure("Does not work");
  };

// 发送预检CORS请求
client
  .request(
    HttpMethod.OPTIONS,
    8080,
    "myserver.mycompany.com",
    "/some-uri")
  .putHeader("Origin", "Server-b.com")
  .putHeader("Access-Control-Request-Method", "POST")
  .expect(methodsPredicate)
  .send()
  .onSuccess(res -> {
    // 立即处理POST请求
  })
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));
提示
响应谓词是在收到响应体 之前 对其进行评估。 因此, 您无法在谓词测试函数中检查response body。

预定义谓词

为了方便起见,Web Client附带了一些常见用例的谓词。

对于状态码,例如 ResponsePredicate.SC_SUCCESS , 验证响应具有 2xx 代码,您也可以自定义创建一个

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

对于content types,例如 ResponsePredicate.JSON , 验证响应具有JSON数据,您也可以自定义创建一个

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

请参考 ResponsePredicate 文档获取预定义谓词的完整列表。

创建自定义失败

默认情况下,响应谓词(包括预定义的)使用默认的错误转换器, 它将丢弃body并传递一条简单消息。您可以通过自定义异常类来替换错误转换器:

ResponsePredicate predicate = ResponsePredicate.create(
  ResponsePredicate.SC_SUCCESS,
  result -> new MyCustomException(result.message()));

许多Web API在错误响应中提供了详细信息。 例如, Marvel API 使用此JSON对象格式:

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

为避免丢失此信息,在错误发生之前, 可以在转换器被调用之前等待响应body被完全接收:

ErrorConverter converter = ErrorConverter.createFullBody(result -> {

  // 响应body被完全接收之后调用
  HttpResponse<Buffer> response = result.response();

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

    // 错误body是JSON数据
    JsonObject body = response.bodyAsJsonObject();

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

  // 返回自定义的消息
  return new MyCustomException(result.message());
});

ResponsePredicate predicate = ResponsePredicate
  .create(ResponsePredicate.SC_SUCCESS, converter);
警告
在Java中,当捕获了stack trace, 创建异常可能会带来性能开销,所以您可能想要创建不捕获stack trace的异常。默认情况下, 报告异常使用不捕获stack trace的异常。

处理 30x 重定向

默认情况下,客户端跟随着重定向,您可以在 WebClientOptions 配置默认行为:

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

客户端最多可以跟随 16 个请求重定向,可以在相同的配置中进行更改:

WebClient client = WebClient
  .create(vertx, new WebClientOptions().setMaxRedirects(5));
注意
出于安全原因,客户端不会使用除了GET或HEAD的方法来跟随着重定向请求

HTTP 响应缓存

Vert.x web 提供了一个可以缓存 HTTP 响应的实现, 您可以创建一个 CachingWebClient 实例。

创建一个带缓存的 web client

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

配置缓存内容

默认情况下,带缓存的 web client 只会缓存通过 GET 方法返回的状态值为 200, 301, 或 404 的响应内容。 带有 Vary 头部的响应默认情况下也不会被缓存。

您可以在创建客户端时,通过传入一个 CachingWebClientOptions 实例来配置缓存策略。

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

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

如果响应的 Cache-Control 头部内容为 private 策略,那么该响应不会被缓存,除非 该 web client 是一个 WebClientSession 的实例。 请参阅 处理私有响应

使用别的储存实现

默认情况下, 带缓存的客户端使用一个本地的 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 缓存策略的 响应,会被缓存到和 web client 一起创建的 CacheStore 中。 私有响应,也就是 Cache-Control 头部中指定为 private 缓存策略的响应,会被缓存到 会话中,以确保缓存的响应不会被泄露给其他用户(会话)。

要创建一个可以缓存私有响应的客户端,需要传入一个 CachingWebClientWebClientSession

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

URI模板

URI模板基于https://datatracker.ietf.org/doc/html/rfc6570[URI Template RFC 6570]提供Http请求字符串URI替代方案。

您可通过阅读 Vert.x URI 文档 以获取更多信息。

您可以使用`UriTemplate`URI替代java字符串URI创建`HttpRequest`,

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

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

然后使用它来创建一个request

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把模板扩展为带有请求模板参数字符串。

字符串模板负责把您的参数encoding,

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

模板参数值

目标参数接受 String, List<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 Client 可以用跟 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。

设置会话级别headers

您可以按以下步骤设置任何会话级别的headers到要添加的每个请求:

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

然后 headers 将被添加到每个请求中; 注意 这些 headers 将发送给所有主机; 如果您需要发送不同的 headers 到不同的主机, 您必须将它们手动添加到每个单个请求中,并且不能添加到 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。

避免过期的 token

您可以使用以下代码来给每个请求设置 token 的过期时间:

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

如果要发送一个请求,那么当前正在使用的用户对象会被检查是否到达了设置的过期时间。 如果需要的话,客户端会刷新 token,否则中止请求并抛出异常。

请求仍然可能因为 token 过期而失败,这是因为服务端也会验证一遍 token 是否 过期。 为了减少用户端的工作, 客户端可以配置为当返回的状态码为 401 (禁止访问)时 只进行 单次 重试。 当 refreshTokenOnForbidden 被设置为 true 时, 客户端会 用一个新的 token 重试一次,再给用户返回 handler/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 提供了RX化的原始版本API, rxSend 方法返回一个能够订阅到HTTP请求的 Single<HttpResponse<Buffer>> ,因此,Single 能被订阅多次。

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

// Single订阅后,发送请求
single.subscribe(response -> System.out.println("Received 1st response with status code" + response.statusCode()), error -> System.out.println("Something went wrong " + error.getMessage()));

// 发送另一个请求
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);

// 使用flatMap操作,向Single的URL上发出请求
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());
});

rxSendStream 应首选发送 Flowable<Buffer> body。

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 将被订阅,其内容被用于请求。

域套接字

自3.7.1后, Web Client 支持domain sockets, 例如,您可以跟 local Docker daemon 交流。

为了达成这个目的, Vertx 实例必须使用native transport创建, 您可以阅读 Vert.x 核心文档,它清楚地说明了这一点。

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

// 我们仍然需要指定主机和端口,
// 因此请求的HTTP header 是 localhost:8080
// 否则将是错误格式的HTTP请求
// 在此示例中,实际值并不重要
client
  .request(
    HttpMethod.GET,
    serverAddress,
    8080,
    "localhost",
    "/images/json")
  .expect(ResponsePredicate.SC_ACCEPTED)
  .as(BodyCodec.jsonObject())
  .send()
  .onSuccess(res ->
    System.out.println("Current Docker images" + res.body()))
  .onFailure(err ->
    System.out.println("Something went wrong " + err.getMessage()));