Web 与 HTTP 服务
你当前正在浏览历史版本 Vert.x 4.1.8 的文档。 访问本页面的 最新版本。

Vert.x Web Client 响应式 Web 客户端

Vert.x Web Client 是一个专为 Vert.x 打造的异步非阻塞 HTTP 与 HTTP/2 客户端。

Web Client 极大地简化了与外部 HTTP 服务器之间的请求/响应交互,并提供了大量高级实用特性:

  • 自动 JSON 请求体编码与响应体反序列化

  • 流式请求/响应管道传输(Pumping)

  • 链式流畅的请求参数追加

  • 统一的强类型异常与断言拦截(Expectations)

  • 各种形式的表单与大文件流式提交(Multipart)

Web Client 并没有废弃 Vert.x Core 的原生 HttpClient,事实上它正是基于 HttpClient 构建的高级封装,继承了其完备的底层能力,如高性能连接池管理、HTTP/2 多路复用、HTTP Pipelining 等。 当需要对 HTTP 报文进行极其微观、低阶的控制时,可以使用底层的 HttpClient。

Web Client 本身不提供 WebSocket API,若需使用 WebSocket,请直接使用 Vert.x Core 的 HttpClient。

使用 Web Client

在项目构建配置中添加以下依赖:

  • Maven(在 pom.xml 中):

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

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

回顾 Vert.x Core HTTP 客户端

Vert.x Web Client 深度基于 Vert.x Core 的底层能力构建。如果你此前未接触过 HttpClient,建议先行查阅 Core 的 HTTP 文档。

创建 Web Client 实例

使用默认配置创建 WebClient 实例:

WebClient client = WebClient.create(vertx);

如需定制高级选项(如连接超时、User-Agent 等):

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

WebClientOptions 继承自 HttpClientOptions,因此可以自由配置底层的各种连接池大小、SSL 凭证等属性。

若应用中已经存在现成的 HttpClient 实例,也可以直接将其包装为 WebClient:

WebClient client = WebClient.wrap(httpClient);
在绝大多数生产场景中,WebClient 应当在应用程序启动时全局创建一次并持久复用。否则你将失去底层连接池复用带来的巨大性能红利,并且若未妥善关闭还会导致套接字资源泄漏。

发起 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("收到响应,状态码: " + response.statusCode()))
  .onFailure(err ->
    System.out.println("请求发生异常: " + err.getMessage()));

// 发起 HEAD 请求
client
  .head(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  .onSuccess(response -> System.out
    .println("收到响应,状态码: " + response.statusCode()))
  .onFailure(err ->
    System.out.println("请求发生异常: " + err.getMessage()));

你可以以链式调用的方式向请求 URI 中动态追加查询参数:

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .addQueryParam("param", "param_value")
  .send()
  .onSuccess(response -> System.out
    .println("收到响应,状态码: " + response.statusCode()))
  .onFailure(err ->
    System.out.println("请求发生异常: " + 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(…​),此前已存在的查询参数会被全部重置。

写入请求体

当需要发送带有负载的请求时,调用对应的 sendXXX 方法即可:

使用 sendBuffer 发送二进制 Buffer:

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

针对大文件或高并发场景,为避免一次性将整个文件读入内存造成内存溢出,Web Client 允许通过 sendStream 直接以反应式流(如 AsyncFile)流式发送:

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

Web Client 会自动建立流传输管道。若流的总长度未知,默认会采用 HTTP 分块传输编码(Chunked Transfer Encoding)。如果预先已知体积,可显式设置 content-length 标头以避免 chunked。

发送 JSON 请求体

发送 JsonObject 时使用 sendJsonObject:

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

也可以通过 sendJson 直接传入任意普通 Java POJO 对象,底层会自动调用 Jackson 进行序列化:

client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendJson(new User("Dale", "Cooper"))
  .onSuccess(res -> {
    // 发送成功
  });

表单提交与文件上传

通过 sendForm 提交常规表单数据:

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

// 默认以 application/x-www-form-urlencoded 格式提交
client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendForm(form)
  .onSuccess(res -> {
    // 提交成功
  });

如需上传文件与复杂属性,使用 MultipartForm 配合 sendMultipartForm:

MultipartForm form = MultipartForm.create()
  .attribute("imageDescription", "一张高清美图")
  .binaryFileUpload(
    "imageFile",
    "image.jpg",
    "/path/to/image",
    "image/jpeg");

client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendMultipartForm(form)
  .onSuccess(res -> {
    // 成功上传
  });

设置 HTTP 请求头

可以通过 MultiMap 批量管理请求头,或使用 putHeader:

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

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

请求认证配置 (Authentication)

支持快捷注入 HTTP 认证凭据:

对于 HTTP Basic 认证:

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

对于 OAuth 2.0 / JWT Bearer 令牌认证:

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

请求模板复用

send 方法可以安全地被多次触发调用。但请注意 HttpRequest 实例内部是可变的,若需在已配置的请求模板上派生新请求,请调用 copy() 进行克隆:

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

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

// 基于克隆副本作差异化修改并发送,原始 get 对象不受污染
get.copy()
  .putHeader("a-header", "with-some-value")
  .send();

超时控制 (Timeouts)

支持配置建立连接超时(Connect Timeout)、请求空闲超时(Idle Timeout)以及全局综合超时(Timeout):

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .timeout(5000) // 5 秒全局超时
  .send()
  .onSuccess(res -> {
    // 成功收到响应
  })
  .onFailure(err -> {
    // 若因超时触发,cause 类型为 java.util.concurrent.TimeoutException
  });

处理 HTTP 响应

发送请求后,返回的结果由 HttpResponse 承载。

默认情况下,只有在发生底层网络或套接字异常时,Vert.x Web Client 的 Future 才会进入 onFailure。 换言之,404 Not Found 或 500 Server Error 仍然属于合规的 HTTP 响应,会被判定为 onSuccess。 若希望当状态码不符合预期时自动将其转换为失败并走 onFailure,请使用下文介绍的 响应断言机制。

响应体解码 (Decoding responses)

默认返回原始二进制 Buffer。通过 BodyCodec 可自动将其解码为目标业务类型:

  • 解码为纯文本 String

  • 解码为 JsonObject

  • 解码为 Java POJO 强类型实体类

  • 流式管道推送至 WriteStream(如直接落盘为文件)

反序列化为 POJO 示例:

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .as(BodyCodec.json(User.class))
  .send()
  .onSuccess(res -> {
    User user = res.body();
    System.out.println("用户姓名: " + user.getFirstName() + " " + user.getLastName());
  });

直接流式落盘保存到文件(避免内存堆积):

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .as(BodyCodec.pipe(fileWriteStream))
  .send();

SSE 服务端事件流 (Server-Sent Events)

通过 BodyCodec.sseStream 可以将长连接的 SSE 事件流直接解码为持续接收事件的响应流:

client.get("/events").as(BodyCodec.sseStream(stream -> {
  stream.handler(event -> System.out.println("收到 SSE 事件: " + event));
  stream.endHandler(v -> System.out.println("SSE 流结束"));
})).send().expecting(HttpResponseExpectation.SC_OK);

响应断言机制 (Response expectations)

通过设置断言(Expectations),可以让 Web Client 在响应状态码或返回的 Content-Type 不符合预期时自动触发失败:

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send()
  // 要求状态码必须为 2xx,且 Content-Type 必须为 application/json
  .expecting(HttpResponseExpectation.SC_SUCCESS.and(HttpResponseExpectation.JSON))
  .onSuccess(res -> {
    // 此时已安全保证为 200 JSON 响应
    JsonObject body = res.bodyAsJsonObject();
  })
  .onFailure(err -> {
    // 当服务端返回 404/500 或非 JSON 时自动进入此处
  });

客户端负载均衡 (Client side load balancing)

当域名解析出多个目标 IP 时,Web Client 默认使用首个 IP。 你可以配置开启客户端负载均衡策略:

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

内置策略包括:轮询(Round-robin)、最少活跃请求(Least requests)、两选择幂(Power of two choices)以及一致性哈希(Consistent hashing)。

HTTP 响应客户端缓存 (CachingWebClient)

通过使用 CachingWebClient,可以为客户端赋予符合 HTTP 规范的响应缓存能力:

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

默认对 200、301、404 的 GET 响应进行缓存,支持自定义缓存条目上限与第三方存储介质(CacheStore SPI)。

URI 模板化支持 (RFC 6570)

支持 RFC 6570 标准的 URI 模板语法,自动进行安全的变量转义与格式化展开:

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

client.get(template)
  .setTemplateParam("amount", "100")
  .setTemplateParam("currency", "EUR")
  .send();

WebClientSession 会话与 Cookie 自动管理

在需要维持类似浏览器 Cookie 会话的场景下,使用 WebClientSession 代替普通 WebClient:

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

// 发送登录请求后,服务端下发的 Cookie 会被 session 自动保存
// 后续请求会自动带上 Cookie 保持会话
session.get("/user/profile").send();

OAuth2 自动令牌管理 (OAuth2WebClient)

使用 OAuth2WebClient 能够全自动管理 OAuth2 Access Token 的下发、鉴权头自动注入,以及在令牌过期(或遇到 401 时)自动触发刷新重试:

OAuth2WebClient oauth2Client = OAuth2WebClient.create(
  baseClient,
  oAuth2Auth,
  new OAuth2WebClientOptions()
    .setRenewTokenOnForbidden(true)); // 遇到 401 自动刷新 Token 并静默重试

Unix Domain Sockets 本地套接字支持

Web Client 原生支持通过 Unix Domain Socket 进行高性能跨进程通信(如直接与本机的 /var/run/docker.sock Docker 守护进程通信):

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

client
  .request(
    HttpMethod.GET,
    serverAddress,
    8080,
    "localhost",
    "/images/json")
  .as(BodyCodec.jsonObject())
  .send()
  .expecting(HttpResponseExpectation.SC_ACCEPTED)
  .onSuccess(res -> System.out.println("本地 Docker 镜像列表: " + res.body()));