1. 程式人生 > 其它 >Spring WebClient 使用簡介

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 建立。

結論

在這篇文章中,我們瞭解瞭如何使用 WebClientDefaultUriBuilder 構建不同型別的 URI。在此過程中,我們介紹了各種型別和格式的查詢引數。最後,我們更改了 URL 構建器的預設編碼模式。


標題Spring WebClient 使用簡介
作者末日沒有進行曲
連結link
時間:2021-02-12
宣告:本部落格所有文章均採用 CC BY-NC-SA 4.0 許可協議,轉載請註明出處。