Spring WebClient 使用簡介
現在,越來越多的專案都開始使用反應式程式設計以及非同步處理請求了。在 Spring 5
中,引入了反應式 WebClient
實現作為 WebFlux
框架的一部分。今天,我們就來學習下如何使用 WebClient
反應式地請求 REST API。
定義 REST API
首先,我們先定義一些 REST API
(假設我們資料庫裡儲存了一系列的事件,這些事件有id、屬性、分類及標籤等):
- /events - 獲取所有的事件
- /events/[id] - 通過 id 獲取事件
- /events/[id]/atrributes/[attributeId] - 通過屬性id獲取某個事件的屬性
- /events?name=[name]&startDate=[startDate] - 根據給定條件獲取事件
- /events?tag[]=[tag1]&tag[]=[tag2] - 根據標籤獲取事件
- /events?category=[category1]&category=[category2] - 根據分類獲取事件
以上,我們定義了一些不同的 URI
。等下,我們就來看下如何使用 WebClient
來構建和傳送每種型別的 URI
。
需要注意的是,通過標籤和分類查詢事件都包含了陣列查詢引數,但是它們的語法不同。因為在 URI
中陣列的表示沒有嚴格定義,這主要取決於服務端的實現。在這裡,我們兩種方式都覆蓋一下。
WebClient 設定
首先,我們需要建立一個WebClient
例項。在這篇文章中,我們使用 mocked
URI
。
我們先定義一個 client 以及相關的 mocked 物件。
this.exchangeFunction = mock(ExchangeFunction.class); ClientResponse mockResponse = mock(ClientResponse.class); when(this.exchangeFunction.exchange(this.argumentCaptor.capture())).thenReturn(Mono.just(mockResponse)); this.webClient = WebClient .builder() .baseUrl("https://example.com/api") .exchangeFunction(exchangeFunction) .build();
在上面的定義中,我們設定了一個引數 baseUrl
,webClient 會將這個引數的值新增到它傳送的所有請求之前。
最後,要驗證特定的 URI 是否已經傳遞給底層的 ExchangeFunction 例項,我們需要使用以下方法:
private void verifyCalledUrl(String relativeUrl) {
ClientRequest request = this.argumentCaptor.getValue();
Assert.assertEquals(String.format("%s%s", BASE_URL, relativeUrl, request.url().toString());
Mockito.verify(this.exchangeFunction).exchange(request);
verifyNoMoreInteractions(this.exchangeFunction);
}
WebClientBuilder
類具有將 UriBuilder 例項作為引數提供的 uri() 方法。通常,API 呼叫通過以下方式進行:
this.webClient.get()
.uri(uriBuilder -> uriBuilder
// ... builder a URI
.build())
.retrieve());
後面,我們將廣泛使用 UriBuilder
來構建 URI
。需要注意的是,我們可以使用任何其它方式構建 URI,然後將生成的 URI 作為字串傳遞進去。
URI 路徑構成
URI 路徑構成由一系列的由斜槓(/)分隔的路徑段組成。首先,我們先看下最簡單的沒有任何可變段的/events
簡單案例。
this.webClient.get()
.uri("/events")
.retrieve();
verifyCalledUrl("/events");
在這個例子中,我們僅傳遞了一個String型別的資料作為引數。
接著,我們使用 /events/{id}
訪問點並構建相應的 URI:
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/events/{id}")
.build(2))
.retrieve();
verifyCalledUrl("/events/2");
從上面的程式碼中,我們可以看到實際的 {id} 值 2 被傳遞給了 biild() 方法。
同樣我們可以為 /events/{id}/attributes/{attributeId}
訪問點建立一個包含多個路徑段的 URI:
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/events/{id}/atrributes/{attributeId}")
.build(3, 12))
.retrieve();
verifyCalledUrl("/events/3/attributes/13");
在最終的 URI 長度沒有超出限制的情況下,一個 URI 可以有很多路徑段。我們只需要確保傳遞給 build() 方法的實際欄位值的順序需要正確。
URI 查詢引數
通常情況下,一個查詢引數是一個簡單的鍵值對,比如 name=dengkaiting。我們來看下如何構建這樣的 URI。
單值引數
我們從單值引數開始,採用 /events?name=[name]&startDate=[startDate]
訪問點。要設定查詢引數,我們需要呼叫 UriBuilder
介面的 queryParam()
方法:
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/events")
.queryParam("name", "InitFailed")
.queryParam("startDate", "13/02/2021")
.build())
.retrieve();
verifyCalledUrl("/events?name=InitFailed&startDate=13/02/2021")
我們添加了兩個查詢引數並給他們設定了實際值。此外,我們也可以使用佔位符而不立即設定實際值:
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/events")
.queryParam("name", "{name}")
.queryParam("startDate", "{startDate}")
.build("InitFailed", "13/02/2021"))
.retrieve();
verifyCalledUrl("/events?name=InitFailed&startTime=13%2F02%2F2021")
當我們需要在呼叫鏈中進一步傳遞引數時,後面這種方式就比較方便了。上面兩個程式碼片段裡有一個重要的區別:
我們可以看到對於預期的 URI,它們兩個的編碼方式是不同的。在後面這個例子中,斜線(/)被轉義了。一般來說,RFC3986
不需要在查詢中對斜槓進行編碼。但是,某些服務端應用可能需要這種轉換。我們在後面會介紹下如果更改此行為。
陣列引數
有時,我們可能需要傳遞一個值陣列。其實,在查詢字串中傳遞陣列並沒有嚴格的限制。對於陣列的表示通常取決於底層框架。下面,我們介紹最廣泛使用的格式。
我們以 /events?tag[]=[tag1]&tag[]=[tag2]
訪問點開始:
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/events")
.queryParam("tag[]", "node", "service")
.build())
.retrieve();
verifyCalledUrl("/events?tag%5B%5D=node&tag%5B%5D=service")
最終的 URL 包含多個標記引數,後面緊跟編碼後的方括號。queryParam()
方法接受可變引數作為值,因此我們不需要多次呼叫該方法。
同樣地,我們還可以省略方括號,只傳遞具有相同鍵但值不同的多個查詢引數 - /events?category=[category1]&category=[category2]
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/events")
.queryParams("category", "info", "warn")
.build())
.retrieve();
verifyCalledUrl("/events?category=info&category=warn")
還有一種更廣泛使用的對陣列進行編碼的方法是傳遞逗號分隔的值。我們把前面的示例轉為逗號分隔值:
this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/events")
.queryParam("category", String.join(",", "info", "warn"))
.build())
.retrieve();
verifyCalledUrl("/events?category=info,warn");
我們只是使用 String 類的 join() 方法建立了一個逗號分隔的字串。當然,我們可以使用應用期望的任何其它分隔符。
編碼模式
我們在上面提到了 URL 的編碼。
如果預設行為不符合我們的要求,我們可以更改它。我們在建立WebClient
例項的時候,可以提供一個 UriBuilderFactory
的實現來更改預設的編碼模式。這裡,我們使用 DefaultBuilderFactory
類。要設定編碼,需要呼叫 setEncodingMode()
方法。可以使用的模式有:
- TEMPLATE_AND_VALUES:對 URI 模板進行預編碼,擴充套件時對 URI 變數進行嚴格編碼
- VALUES_ONLY: 不對 URL 模板進行編碼,而是將 URI 變數展開成模板後嚴格編碼
- URI_COMPONENTS:擴充套件 URI 變數後編碼 URI 的元件值
- NONE:不會應用任何編碼
預設值是 TEMPLATE_AND_VALUES
。我們把模式設定成 URI_COMPONENTS
。
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
this.webClient = WebClient
.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.exchangeFunction(exchangeFunction)
.build();
最終,下面的斷言可以成功:
this.webClient.get()
.uri(uriBuilder -> uriBuilder)
.path("/events")
.queryParam("name", "InitFailed")
.queryParam("startDate", "13/02/2021")
.build()
.retrieve();
verifyCalledUrl("/events?name=InitFailed&startDate=13/02/2021");
當然,我們也可以提供一個完全自定義的 URIBuilderFactory 實現來手動處理 URI 建立。
結論
在這篇文章中,我們瞭解瞭如何使用 WebClient
和 DefaultUriBuilder
構建不同型別的 URI。在此過程中,我們介紹了各種型別和格式的查詢引數。最後,我們更改了 URL 構建器的預設編碼模式。
標題:Spring WebClient 使用簡介
作者:末日沒有進行曲
連結:link
時間:2021-02-12
宣告:本部落格所有文章均採用 CC BY-NC-SA 4.0 許可協議,轉載請註明出處。