1. 概述

本文将深入探讨 Spring 5 引入的响应式 Web 客户端 WebClient,以及专为测试设计的 WebTestClient。我们将通过实际场景分析其核心特性与最佳实践。

2. 什么是 WebClient?

简单来说,WebClient 是执行 Web 请求的统一入口点。作为 Spring Web Reactive 模块的核心组件,它将逐步取代传统的 RestTemplate。关键特性包括:

响应式非阻塞设计:基于 HTTP/1.1 协议
双模式支持:既适用于响应式栈(Reactive Stack),也兼容 Servlet 栈(通过阻塞操作获取结果)
⚠️ 注意:在响应式环境中应避免阻塞操作,否则违背设计初衷

其唯一实现类是 DefaultWebClient,后续示例均基于此展开。

3. 依赖配置

3.1. Maven 配置

pom.xml 添加核心依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

3.2. Gradle 配置

build.gradle 中添加:

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-webflux'
}

4. WebClient 实战指南

掌握 WebClient 需理解三个核心环节:实例创建请求构建响应处理

4.1. 创建 WebClient 实例

提供三种创建方式:

方式一:默认配置

WebClient client = WebClient.create();

方式二:指定基础 URI

WebClient client = WebClient.create("http://localhost:8080");

方式三:高级定制(推荐)

WebClient client = WebClient.builder()
  .baseUrl("http://localhost:8080")
  .defaultCookie("cookieKey", "cookieValue")
  .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
  .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
  .build();

4.2. 配置超时参数

默认 30 秒超时往往不满足需求,需自定义 HttpClient

HttpClient httpClient = HttpClient.create()
  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
  .responseTimeout(Duration.ofMillis(5000))
  .doOnConnected(conn -> 
    conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
      .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));

WebClient client = WebClient.builder()
  .clientConnector(new ReactorClientHttpConnector(httpClient))
  .build();

⚠️ 踩坑提示timeout() 方法设置的是信号超时(Mono/Flux 发布超时),而非 HTTP 连接/读写超时!

4.3. 定义请求方法

两种方式指定 HTTP 方法:

通用方式

UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);

快捷方法

UriSpec<RequestBodySpec> uriSpec = client.post();

注意:请求规范对象(如 UriSpec)不可复用!每次请求需重新创建。

4.4. 定义请求 URL

三种 URL 设置方式:

直接字符串

RequestBodySpec bodySpec = uriSpec.uri("/resource");

UriBuilder 函数式

RequestBodySpec bodySpec = uriSpec.uri(
  uriBuilder -> uriBuilder.pathSegment("/resource").build());

URI 对象

RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));

4.5. 定义请求体

四种主流请求体设置方式:

方式一:直接值

RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");

方式二:Publisher

RequestHeadersSpec<?> headersSpec = bodySpec.body(
  Mono.just(new Foo("name")), Foo.class);

方式三:BodyInserters(值)

RequestHeadersSpec<?> headersSpec = bodySpec.body(
  BodyInserters.fromValue("data"));

方式四:BodyInserters(Publisher)

RequestHeadersSpec headersSpec = bodySpec.body(
  BodyInserters.fromPublisher(Mono.just("data")),
  String.class);

高级场景:文件上传

LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
  BodyInserters.fromMultipartData(map));

核心概念BodyInserter 负责填充响应体,Publisher(如 Mono/Flux)提供数据流。

4.6. 定义请求头

设置头信息会与实例化时的默认值合并:

ResponseSpec responseSpec = headersSpec.header(
    HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
  .accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
  .acceptCharset(StandardCharsets.UTF_8)
  .ifNoneMatch("*")
  .ifModifiedSince(ZonedDateTime.now())
  .retrieve();

4.7. 获取响应

两种响应处理模式:

模式一:完整响应控制(exchangeToMono/exchangeToFlux)

Mono<String> response = headersSpec.exchangeToMono(response -> {
  if (response.statusCode().equals(HttpStatus.OK)) {
      return response.bodyToMono(String.class);
  } else if (response.statusCode().is4xxClientError()) {
      return Mono.just("Error response");
  } else {
      return response.createException()
        .flatMap(Mono::error);
  }
});

模式二:直接获取响应体(retrieve)

Mono<String> response = headersSpec.retrieve()
  .bodyToMono(String.class);

⚠️ 注意retrieve() 在 4xx/5xx 状态码时会抛出 WebClientException

5. WebTestClient 测试实战

WebTestClient 是测试 WebFlux 接口的利器,API 与 WebClient 高度相似。

5.1. 绑定真实服务器

端到端集成测试:

WebTestClient testClient = WebTestClient
  .bindToServer()
  .baseUrl("http://localhost:8080")
  .build();

5.2. 绑定路由函数

测试特定 RouterFunction:

RouterFunction function = RouterFunctions.route(
  RequestPredicates.GET("/resource"),
  request -> ServerResponse.ok().build()
);

WebTestClient
  .bindToRouterFunction(function)
  .build().get().uri("/resource")
  .exchange()
  .expectStatus().isOk()
  .expectBody().isEmpty();

5.3. 绑定 Web 处理器

WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();

5.4. 绑定应用上下文

自动扫描控制器和 @EnableWebFlux 配置:

@Autowired
private ApplicationContext context;

WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
  .build();

5.5. 绑定控制器

直接注入控制器测试:

@Autowired
private Controller controller;

WebTestClient testClient = WebTestClient.bindToController(controller).build();

5.6. 执行测试请求

典型测试链式调用:

WebTestClient
  .bindToServer()
    .baseUrl("http://localhost:8080")
    .build()
    .post()
    .uri("/resource")
  .exchange()
    .expectStatus().isCreated()
    .expectHeader().valueEquals("Content-Type", "application/json")
    .expectBody().jsonPath("field").isEqualTo("value");

6. 总结

WebClient 作为 Spring 5 的响应式 HTTP 客户端,通过以下优势革新了客户端请求处理:

  1. 统一编程模型:同步/异步双模式支持
  2. 非阻塞架构:基于 Reactor 响应式流
  3. 灵活配置:超时/拦截器/编解码器深度定制
  4. 测试友好:WebTestClient 提供专用测试 API

完整代码示例见 GitHub 仓库


原始标题:Spring 5 WebClient