知識庫 / HTTP Client-Side RSS 訂閱

Spring WebClient 使用帶參數請求

HTTP Client-Side,Spring Web
HongKong
5
01:21 PM · Dec 06 ,2025

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. 結論

在本文中,我們學習瞭如何使用 WebClientDefaultUriBuilder 構建不同類型的 URI。

在過程中,我們涵蓋了各種類型和格式的查詢參數。最後,我們通過更改 URL 構建器的默認編碼模式來總結全文。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.