1. 概述
大量的框架和項目正在引入 反應式編程和異步請求處理。因此,Spring 5 引入了反應式 WebClient 實現作為 WebFlux 框架的一部分。
在本教程中,我們將學習如何通過 反應式方式消費 REST API 端點,並使用 WebClient。
2. REST API 端點
首先,讓我們定義一個示例 REST API,包含以下 GET 端點:
- /products – 獲取所有產品
- /products/{id} – 根據 ID 獲取產品
- /products/{id}/attributes/{attributeId} – 根據 ID 獲取產品屬性
- /products/?name={name}&deliveryDate={deliveryDate}&color={color} – 按名稱、送達日期和顏色查找產品
- /products/?tag[]={tag1}&tag[]={tag2} – 按標籤獲取產品
- /products/?category={category1}&category={category2} – 按類別獲取產品
這裏我們定義了幾個不同的 URI。稍後,我們將學習如何使用 WebClient 構建和發送每種類型的 URI。
請注意,獲取產品按標籤和類別時,URI 中的查詢參數包含數組;但是,語法不同,因為 沒有嚴格定義數組在 URI 中應該如何表示。這主要取決於服務器端實現。因此,我們將涵蓋這兩種情況。
3. WebClient 設置
首先,我們需要創建一個 WebClient 的實例。 在本文中,我們將使用一個模擬對象來驗證是否請求了有效的 URI。
以下是如何定義客户端和相關模擬對象:
exchangeFunction = mock(ExchangeFunction.class);
ClientResponse mockResponse = mock(ClientResponse.class);
when(mockResponse.bodyToMono(String.class))
.thenReturn(Mono.just("test"));
when(exchangeFunction.exchange(argumentCaptor.capture()))
.thenReturn(Mono.just(mockResponse));
webClient = WebClient
.builder()
.baseUrl("https://example.com/api")
.exchangeFunction(exchangeFunction)
.build();我們還會傳遞一個基礎 URL,該 URL 將被附加到客户端發出的所有請求。
最後,為了驗證特定的 URI 是否已傳遞到底層的 ExchangeFunction 實例,我們將使用以下輔助方法:
private void verifyCalledUrl(String relativeUrl) {
ClientRequest request = argumentCaptor.getValue();
assertEquals(String.format("%s%s", BASE_URL, relativeUrl), request.url().toString());
verify(this.exchangeFunction).exchange(request);
verifyNoMoreInteractions(this.exchangeFunction);
}WebClientBuilder 類具有 uri() 方法,該方法將 UriBuilder 實例作為參數提供。通常,我們以以下方式進行 API 調用:
webClient.get()
.uri(uriBuilder -> uriBuilder
//... building a URI
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();我們將廣泛使用 UriBuilder 在本指南中構建 URI。 值得注意的是,我們也可以使用其他方法構建 URI,然後將生成的 URI 作為字符串傳遞。
4. URI 路徑組件
路徑組件由用斜槓 ( / ) 分隔的路徑片段序列組成。 首先,我們以一個簡單的例子開始,該 URI 沒有變量片段:/products
webClient.get()
.uri("/products")
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products");對於本例,我們只需將一個 String 作為參數傳遞即可。
接下來,我們將使用 /products/{id} 端點並構建相應的 URI:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/{id}")
.build(2))
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/2");
從上面的代碼中可以看出,實際的片段值傳遞給 build() 方法。
類似地,我們可以為 /products/{id}/attributes/{attributeId} 端點創建一個包含多個路徑片段的 URI:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/{id}/attributes/{attributeId}")
.build(2, 13))
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/2/attributes/13");URI 可以包含任意數量的路徑片段,但最終 URI 的長度必須不超過限制。 此外,我們還需要記住在將實際片段值傳遞給 build() 方法時保持正確的順序。
5. URI 查詢參數
通常,查詢參數是一個簡單的鍵值對,例如 title=Baeldung。 讓我們看看如何構建這樣的 URI。
5.1. 單個值參數
我們將從單個值參數開始,並使用 /products/?name={name}&deliveryDate={deliveryDate}&color={color} 端點。 要設置查詢參數,我們將調用 UriBuilder 接口中的 queryParam() 方法:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "AndroidPhone")
.queryParam("color", "black")
.queryParam("deliveryDate", "13/04/2019")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");
我們在此添加了三個查詢參數並立即分配了實際值。反過來,也可以使用佔位符代替實際值:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "{title}")
.queryParam("color", "{authorId}")
.queryParam("deliveryDate", "{date}")
.build("AndroidPhone", "black", "13/04/2019"))
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13%2F04%2F2019");
這在將構建器對象傳遞到鏈中時尤其有用。
請注意,以上兩個代碼片段之間存在一個重要的 差異。 考慮到預期的 URI,我們可以看到它們被編碼方式不同。 尤其是,斜槓字符 ( / ) 在最後一個示例中被轉義了。
一般來説,RFC3986 不要求在查詢中編碼斜槓;但是,某些服務端應用程序可能需要進行此類轉換。 因此,我們將在本指南的後續部分中學習如何更改此行為。
5.2. 數組參數
我們可能需要傳遞一個值的數組,對於在查詢字符串中傳遞數組,並沒有嚴格的規則。因此,不同項目在查詢字符串中的數組表示形式各不相同,通常取決於底層框架。本文將介紹目前最常用的格式。
讓我們從以下端點開始:/products/?tag[]={tag1}&tag[]={tag2}
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("tag[]", "Snapdragon", "NFC")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?tag%5B%5D=Snapdragon&tag%5B%5D=NFC");如我們所見,最終 URI 包含多個標籤參數,後跟編碼後的方括號。 queryParam() 方法接受變量參數作為值,因此無需多次調用該方法。
或者,我們可以 省略方括號並僅傳遞具有相同鍵的不同查詢參數,例如:/products/?category={category1}&category={category2}:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("category", "Phones", "Tablets")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?category=Phones&category=Tablets");最後,還有一種被廣泛使用的數組編碼方法,即通過傳遞逗號分隔的值。讓我們將我們之前的示例轉換為逗號分隔的值:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("category", String.join(",", "Phones", "Tablets"))
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?category=Phones,Tablets");我們使用 join() 方法來創建逗號分隔的字符串,該方法是 String 類中的一個方法。我們還可以使用任何其他分隔符,只要它符合應用程序的要求。
6. 編碼模式
請記住我們之前提到過的 URL 編碼嗎?
如果默認行為不符合我們的要求,我們可以更改它。我們需要在構建 WebClient 實例時提供一個 UriBuilderFactory 實現。在這種情況下,我們將使用 DefaultUriBuilderFactory 類。要設置編碼,我們將調用 setEncodingMode() 方法。以下模式可用:
- TEMPLATE_AND_VALUES:預編碼 URI 模板,並在展開時嚴格編碼 URI 變量
- VALUES_ONLY:不編碼 URI 模板,但在展開後嚴格編碼 URI 變量
- URI_COMPONENTS:展開 URI 變量後編碼 URI 組件值
- NONE:不進行任何編碼
默認值為 TEMPLATE_AND_VALUES。讓我們將模式設置為 URI_COMPONENTS:
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
webClient = WebClient
.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.exchangeFunction(exchangeFunction)
.build();因此,以下斷言將成功:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "AndroidPhone")
.queryParam("color", "black")
.queryParam("deliveryDate", "13/04/2019")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");當然,我們還可以提供完全自定義的 UriBuilderFactory 實現,用於手動處理 URI 創建。
7. 結論
在本文中,我們學習瞭如何使用 WebClient 和 DefaultUriBuilder 構建不同類型的 URI。
在過程中,我們涵蓋了各種類型和格式的查詢參數。最後,我們通過更改 URL 構建器的默認編碼模式來總結全文。