1、概览 {#1概览}
本文将带你了解如何使用 WebClient
和 WebTestClient
,前者是一个 Spring 5 中引入的响应式 HTTP 客户端,而后者是一种用于测试的 WebClient
。
2、WebClient 是啥? {#2webclient-是啥}
简而言之,WebClient
是一个接口,表示执行 Web 请求的主要入口点。
它是 Spring Web Reactive 模块的一部分,用于取代经典的 RestTemplate
。此外,这个新的客户端是一个基于 HTTP/1.1 协议的响应式、非阻塞解决方案。
尽管它实际上是一个非阻塞客户端,而且属于 spring-webflux 库,但该解决方案同时支持 同步 和 异步 操作,因此也适用于在 Servlet 技术栈上运行的应用,通过阻塞操作来获得结果。当然,如果使用的是响应式技术栈,则不建议采用这种做法。
该接口只有一个实现,即我们将要使用的 DefaultWebClient
类。
3、依赖 {#3依赖}
在 Spring Boot 应用中,只需要添加 spring-boot-starter-webflux
依赖即可获得响应式 Web 的支持。
3.1、使用 Maven 构建 {#31使用-maven-构建}
在 pom.xml
中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
3.2、使用 Gradle 构建 {#32使用-gradle-构建}
build.gradle
文件中添加以下条目:
dependencies {
compile 'org.springframework.boot:spring-boot-starter-webflux'
}
4、使用 WebClient {#4使用-webclient}
要使用 WebClient,我们需要了解如何:
- 创建实例
- 发起请求
- 处理响应
4.1、创建 WebClient 实例 {#41创建-webclient-实例}
有三种方式可供选择。第一种是使用默认设置创建 WebClient
对象:
WebClient client = WebClient.create();
第二种方式是使用给定的基本 URI 初始化 WebClient
实例:
WebClient client = WebClient.create("http://localhost:8080");
第三种方式(推荐)是使用 DefaultWebClientBuilder
类构建客户端,该类允许完全自定义:
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、指定超时时间 {#42指定超时时间}
通常情况下,默认的 30 秒 HTTP 超时时间太慢,无法满足需要,要自定义这种行为,可以创建一个 HttpClient
实例,并配置 WebClient
使用它。
- 通过
ChannelOption.CONNECT_TIMEOUT_MILLIS
选项设置连接超时。 - 分别使用
ReadTimeoutHandler
和写WriteTimeoutHandler
设置读、写超时。 - 使用
responseTimeout
指令配置响应超时。
所有这些都必须在要配置的 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
,但这是信号超时,而不是 HTTP 连接、读/写或响应超时;这是 Mono
/ Flux
Publisher 的超时。
4.3、准备请求 - 定义方法 {#43准备请求---定义方法}
首先,需要通过调用 method(HttpMethod method)
来指定请求的 HTTP 方法:
UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);
或调用其快捷方法,如 get
、post
和 delete
:
UriSpec<RequestBodySpec> uriSpec = client.post();
注意 :虽然看起来重用了 Request Spec 变量(WebClient.UriSpec
、WebClient.RequestBodySpec
、WebClient.RequestHeadersSpec
、WebClient.ResponseSpec
),但这只是为了简化演示不同的方法。这些指令不应该在不同请求中重复使用,因为它们会检索引用,因此后面的操作会修改在前面步骤中所定义的内容。
4.4、准备请求 - 定义 URL {#44准备请求---定义-url}
下一步是提供 URL。同样,也有不同的方法。
可以将其作为字符串传递给 uri
API:
RequestBodySpec bodySpec = uriSpec.uri("/resource");
使用 UriBuilder
Function 接口:
RequestBodySpec bodySpec = uriSpec.uri(
uriBuilder -> uriBuilder.pathSegment("/resource").build());
或者,传递一个 java.net.URL
实例:
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));
注意 ,如果为 WebClient
定义了默认的基本 URL,那么最后一个方法将覆盖该值。
4.5、准备请求 - 定义请求体 {#45准备请求---定义请求体}
然后,可以根据需要设置请求体(Body)、Content Type、Length、Cookie 或 Header。
例如,如果要设置请体,有几种可用的方法。最常见、最直接的方法可能就是使用 bodyValue
方法:
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");
或者通过将一个 Publisher
(以及将要发布的元素类型)传递给 body
方法来实现:
RequestHeadersSpec<?> headersSpec = bodySpec.body(
Mono.just(new Foo("name")), Foo.class);
另外,还可以使用 BodyInserters
工具类。例如,来看看如何像使用 bodyValue
方法那样,使用一个简单的对象来作为请求体。
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromValue("data"));
同样,如果使用的是 Reactor
实例,也可以使用 BodyInserters#fromPublisher
方法:
RequestHeadersSpec headersSpec = bodySpec.body(
BodyInserters.fromPublisher(Mono.just("data")),
String.class);
该类还提供其他直观的功能,以覆盖更高级的应用场景。例如,发送 multipart 请求:
LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromMultipartData(map));
所有这些方法都会创建一个 BodyInserter
实例,然后可以将其作为请求体(Request Body)。
BodyInserter
是一个接口,负责用给定的输出消息和插入时使用的 Context 填充 ReactiveHttpOutputMessage
。
Publisher
是一个响应式组件,负责提供数量可能无限的序列化元素。它也是一个接口,最常用的实现是 Mono
和 Flux
。
4.6、准备请求 - 定义 Header {#46准备请求---定义-header}
设置 body 后,可以设置 Header、Cookie 和可接受的媒体类型。这些值将添加到实例化客户端时已设置的值中。
此外,还为 If-None-Match
、If-Modified-Since
、Accept
和 Accept-Charset
等最常用的 Header 提供了额外支持。
示例如下:
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、获取响应 {#47获取响应}
最后一个阶段是发送请求并接收响应。可以使用 exchangeToMono
/ exchangeToFlux
或 retrieve
方法来实现。
exchangeToMono
和 exchangeToFlux
方法允许访问 ClientResponse
及其 HTTP 状态码和 Header:
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
方法是直接获取响应 Body 的最便捷的方式:
Mono<String> response = headersSpec.retrieve()
.bodyToMono(String.class);
需要注意 ResponseSpec.bodyToMono
方法,如果状态码为 4xx
(客户端错误)或 5xx
(服务器错误),该方法将抛出 WebClientException
。
5、使用 WebTestClient {#5使用-webtestclient}
WebTestClient
是测试 WebFlux 服务器端点的主要入口。它的 API 与 WebClient
非常相似,它将大部分工作委托给内部 WebClient
实例,主要侧重于提供测试上下文。DefaultWebTestClient
类是 WebTestClient
的唯一接口实现。
用于测试的客户端可以绑定到真正的服务器上,也可以与特定的 Controller 或 Function 一起使用。
5.1、绑定到服务器 {#51绑定到服务器}
要通过对运行服务器的实际请求完成端到端的集成测试,可以使用 bindToServer
方法:
WebTestClient testClient = WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build();
5.2、绑定到路由 {#52绑定到路由}
可以通过将特定 RouterFunction
传递给 bindToRouterFunction
方法来测试该 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、绑定到 WebHandler {#53绑定到-webhandler}
使用 bindToWebHandler
方法可以实现相同的行为,该方法需要一个 WebHandler
实例:
WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();
5.4、绑定到 ApplicationContext {#54绑定到-applicationcontext}
使用 bindToApplicationContext
方法时,会出现更有趣的情况。该方法接收 ApplicationContext
,并解析 Context 中的 Controller Bean 和 @EnableWebFlux
配置。
注入一个 ApplicationContext
实例,示例如下:
@Autowired
private ApplicationContext context;
WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
.build();
5.5、绑定到 Controller {#55绑定到-controller}
一种更简便的方法是通过 bindToController
方法提供要测试的 Controller 数组。假设已经有了一个 Controller 类,并其注入到了一个需要的类中。如下:
@Autowired
private Controller controller;
WebTestClient testClient = WebTestClient.bindToController(controller).build();
5.6、发起请求 {#56发起请求}
构建 WebTestClient
对象后,所有后续操作都与 WebClient
类似,直到 exchange
方法(获取响应的一种方法),该方法提供了 WebTestClient.ResponseSpec
接口,可使用诸如 expectStatus
、expectBody
和 expectHeader
等有用的方法进行操作:
WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build()
.post()
.uri("/resource")
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals("Content-Type", "application/json")
.expectBody().jsonPath("field").isEqualTo("value");
Ref:https://www.baeldung.com/spring-5-webclient