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.1.8</version>
</dependency>
  • Gradle (在您的 build.gradle 文件内):

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

回顾 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的响应, 被视为失败。

警告
响应会被完全缓冲,请使用 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()));

这个编码器将响应缓存泵入到 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 简单的丢弃response body。

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的方法来跟随着重定向请求

使用 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

创建一个 WebSession

您像下面一样创建一个 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

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