1. 概述
在本教程中,我們將研究 WebClient,這是一個在 Spring 5 中引入的反應式 Web 客户端。
我們還將探討 WebTestClient,這是一個專為測試設計的 WebClient。
2. 什麼是 WebClient ?
簡單來説,WebClient 是執行 Web 請求的主要入口點表示的接口。
它是在 Spring Web Reactive 模塊中創建的,並將會在這些場景中取代經典的 RestTemplate。 此外,新的客户端是一個反應式、非阻塞的解決方案,它在 HTTP/1.1 協議上工作。
需要注意的是,儘管它是非阻塞客户端,並且屬於 spring-webflux 庫,但該解決方案支持同步和異步操作,因此也適用於在 Servlet 堆棧上運行的應用程序。
可以通過阻塞操作來獲取結果來實現這一點。 當然,如果我們在反應式堆棧上工作,則不建議採用這種做法。
最後,該接口只有一個實現,即 DefaultWebClient 類,我們將使用它。
3. 依賴項
由於我們正在使用 Spring Boot 應用程序,我們只需要 spring-boot-starter-webflux 依賴項,即可獲得 Spring Framework 的 Reactive Web 支持。
3.1. 使用 Maven 構建
讓我們將以下依賴項添加到 pom.xml 文件中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.5.7</vesion>
</dependency>3.2. 使用 Gradle 構建
使用 Gradle 時,我們需要在 build.gradle 文件中添加以下條目:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}4. 使用 WebClient
為了正確地使用客户端,我們需要知道如何:
- 創建實例
- 發起請求
- 處理響應
4.1. 創建 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. 創建帶有超時時間的 WebClient 實例
通常情況下,默認的 HTTP 超時時間(30 秒)對於我們的需求來説過於緩慢。為了自定義此行為,我們可以創建 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. 準備請求 – 定義方法
首先,我們需要通過調用 <imethod(HttpMethod method)> 來指定請求的 HTTP 方法。
UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);或者通過調用其快捷方法,例如 get、post 和 delete:
UriSpec<RequestBodySpec> uriSpec = client.post();注意:雖然看起來我們重用請求規範變量(WebClient.UriSpec、WebClient.RequestBodySpec、WebClient.RequestHeadersSpec、WebClient.ResponseSpec),但這只是為了簡化演示不同的方法。這些指令不應在不同的請求中使用,它們獲取引用,因此後續操作會修改我們在先前步驟中定義的定義。
4.4. 準備請求 – 定義 URL
下一步是提供 URL。 再次強調,有多種方式可以做到這一點。
我們可以將其傳遞給 uri API 作為 String:
RequestBodySpec bodySpec = uriSpec.uri("/resource");使用 UriBuilder 函數:
RequestBodySpec bodySpec = uriSpec.uri(
uriBuilder -> uriBuilder.pathSegment("/resource").build());或者,作為一個 java.net.URL 實例:
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));請注意,如果為 WebClient 定義了默認基本 URL,則此方法將覆蓋該值。
4.5. 準備請求 – 定義請求體
然後我們可以設置請求體、內容類型、長度、Cookie 或 Header,如果需要的話。
例如,如果我們想設置請求體,有幾種可用的方法。 最常見和最直接的選項通常是使用 bodyValue 方法:
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");通過向 body 方法呈現 Publisher (以及將要發佈的元素的類型)來完成。
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);此類還提供其他直觀函數,以覆蓋更高級的場景。例如,如果需要發送多部分請求:
LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromMultipartData(map));所有這些方法都會創建一個 BodyInserter 實例,然後我們可以將其呈現為請求的 主體。
BodyInserter 是一個負責將給定的輸出消息和在插入過程中使用的上下文填充到 ReactiveHttpOutputMessage 主體中的接口。
一個 Publisher 是一個負責提供可能具有無限數量的序列化元素的反應式組件。它也是一個接口,最流行的實現是 Mono 和 Flux。
4.6. 準備請求 – 定義頭部
在設置請求體之後,我們可以設置頭部、Cookie 以及可接受的媒體類型。在實例化客户端時,已設置的值將被添加到其中。
此外,對於最常用的頭部,例如 “If-None-Match”, “If-Modified-Since”, “Accept”, 和 “Accept-Charset”. 提供了支持。
以下是一個這些值如何使用的示例。
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 或 retrieve 方法來實現。
exchangeToMono 和 exchangeToFlux 方法允許訪問 ClientResponse 及其狀態和頭部信息:
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);務必注意 ResponseSpec。 bodyToMono 方法會在狀態碼為 4xx (客户端錯誤) 或 5xx (服務器錯誤) 時拋出 WebClientException。
5. 使用 WebTestClient
WebTestClient 是測試 WebFlux 服務器端點的主要入口。它的 API 與 WebClient 極其相似,並且它將大部分工作委託給一個內部的 WebClient 實例,主要提供測試上下文。DefaultWebTestClient 類是一個單一接口實現。
客户端可以綁定到真實的服務器,也可以與特定的控制器或函數一起工作。
5.1. 與服務器綁定
要完成使用實際請求與正在運行的服務器進行端到端集成測試,可以使用 bindToServer 方法:
WebTestClient testClient = WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build();
5.2. 綁定到路由器
我們可以通過將 RouterFunction 傳遞給 bindToRouterFunction 方法來測試特定的路由器函數:
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 處理程序
可以使用 bindToWebHandler 方法來實現相同的功能,該方法接受一個 WebHandler 實例:
WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();5.4. 綁定到應用程序上下文
當使用 bindToApplicationContext 方法時,會發生更復雜的情況。該方法接受一個 ApplicationContext,並分析上下文中的控制器 Bean 和 @EnableWebFlux 配置。
如果注入一個 ApplicationContext 實例,簡單的代碼片段可能如下所示:
@Autowired
private ApplicationContext context;
WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
.build();
5.5. 綁定到控制器
一種更簡便的方法是提供一個數組,用於通過 bindToController 方法進行控制器的測試。 假設我們已經擁有一個 Controller 類,並且將其注入到需要的類中,我們可以這樣編寫:
@Autowired
private Controller controller;
WebTestClient testClient = WebTestClient.bindToController(controller).build();
5.6. 發送請求
在構建 <i >WebTestClient</i> 對象後,鏈式操作將類似於 <i >WebClient</i>,直到 <i >exchange</i> 方法(一種獲取響應的方式),該方法提供 <i >WebTestClient.ResponseSpec</i> 接口,用於使用諸如 <i >expectStatus</i>, <i >expectBody</i>, 和 <i >expectHeader</i> 等有用的方法:
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 機制,用於在客户端進行請求。
我們還考察了它提供的優勢,通過配置客户端、準備請求和處理響應來完成。