REST 客戶端

Spring 框架提供了以下選項來呼叫 REST 端點

  • RestClient — 具有流式 API 的同步客戶端

  • WebClient — 具有流式 API 的非阻塞式、響應式客戶端

  • RestTemplate — 具有模板方法 API 的同步客戶端,現已棄用,轉而使用 RestClient

  • HTTP 服務客戶端 — 由生成的代理支援的帶註解介面

RestClient

RestClient 是一個同步 HTTP 客戶端,它提供流式 API 來執行請求。它作為 HTTP 庫的抽象,並處理 HTTP 請求和響應內容與高階 Java 物件之間的轉換。

建立 RestClient

RestClient 具有靜態的 create 快捷方法。它還公開了一個 builder(),其中包含更多選項

  • 選擇要使用的 HTTP 庫,請參閱 客戶端請求工廠

  • 配置訊息轉換器,請參閱 HTTP 訊息轉換

  • 設定 baseUrl

  • 設定預設請求頭、Cookie、路徑變數、API 版本

  • 配置 ApiVersionInserter

  • 註冊攔截器

  • 註冊請求初始化器

一旦建立,RestClient 就可以安全地在多個執行緒中使用。

下面展示瞭如何建立或構建 RestClient

  • Java

  • Kotlin

RestClient defaultClient = RestClient.create();

RestClient customClient = RestClient.builder()
	.requestFactory(new HttpComponentsClientHttpRequestFactory())
	.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
	.baseUrl("https://example.com")
	.defaultUriVariables(Map.of("variable", "foo"))
	.defaultHeader("My-Header", "Foo")
	.defaultCookie("My-Cookie", "Bar")
	.defaultVersion("1.2")
	.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
	.requestInterceptor(myCustomInterceptor)
	.requestInitializer(myCustomInitializer)
	.build();
val defaultClient = RestClient.create()

val customClient = RestClient.builder()
	.requestFactory(HttpComponentsClientHttpRequestFactory())
	.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
	.baseUrl("https://example.com")
	.defaultUriVariables(mapOf("variable" to "foo"))
	.defaultHeader("My-Header", "Foo")
	.defaultCookie("My-Cookie", "Bar")
       .defaultVersion("1.2")
       .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
	.requestInterceptor(myCustomInterceptor)
	.requestInitializer(myCustomInitializer)
	.build()

使用 RestClient

要執行 HTTP 請求,首先指定要使用的 HTTP 方法。使用便捷方法,如 get()head()post() 等,或 method(HttpMethod)

請求 URL

接下來,使用 uri 方法指定請求 URI。這是可選的,如果您透過構建器配置了 baseUrl,則可以跳過此步驟。URL 通常指定為 String,帶有可選的 URI 模板變數。以下顯示瞭如何執行請求

  • Java

  • Kotlin

int id = 42;
restClient.get()
	.uri("https://example.com/orders/{id}", id)
	// ...
val id = 42
restClient.get()
	.uri("https://example.com/orders/{id}", id)
	// ...

函式也可以用於更多控制,例如指定請求引數

字串 URL 預設進行編碼,但可以透過使用自定義 uriBuilderFactory 構建客戶端來更改此設定。URL 還可以透過函式或 java.net.URI 提供,這兩種方式都不會進行編碼。有關使用和編碼 URI 的更多詳細資訊,請參閱URI 連結

請求頭和請求體

如有必要,可以透過 header(String, String)headers(Consumer<HttpHeaders>) 或便捷方法 accept(MediaType…​)acceptCharset(Charset…​) 等來新增請求頭來操作 HTTP 請求。對於可以包含請求體(POSTPUTPATCH)的 HTTP 請求,還提供了其他方法:contentType(MediaType)contentLength(long)。如果客戶端配置了 ApiVersionInserter,您可以為請求設定 API 版本。

請求體本身可以透過 body(Object) 設定,它內部使用 HTTP 訊息轉換。或者,請求體可以使用 ParameterizedTypeReference 設定,允許您使用泛型。最後,請求體可以設定為寫入 OutputStream 的回撥函式。

檢索響應

請求設定完成後,可以透過在 retrieve() 後鏈式呼叫方法來發送請求。例如,可以透過使用 retrieve().body(Class)retrieve().body(ParameterizedTypeReference) 來訪問響應體,後者適用於列表等引數化型別。body 方法將響應內容轉換為各種型別——例如,位元組可以轉換為 String,JSON 可以使用 Jackson 轉換為物件,等等(參見 HTTP 訊息轉換)。

響應也可以轉換為 ResponseEntity,透過 retrieve().toEntity(Class) 可以訪問響應頭和響應體

單獨呼叫 retrieve() 是一個無操作,並返回一個 ResponseSpec。應用程式必須對 ResponseSpec 呼叫一個終止操作才能產生任何副作用。如果您的用例對消費響應不感興趣,您可以使用 retrieve().toBodilessEntity()

此示例展示瞭如何使用 RestClient 執行簡單的 GET 請求。

  • Java

  • Kotlin

String result = restClient.get() (1)
	.uri("https://example.com") (2)
	.retrieve() (3)
	.body(String.class); (4)

System.out.println(result); (5)
1 設定 GET 請求
2 指定要連線的 URL
3 檢索響應
4 將響應轉換為字串
5 列印結果
val result= restClient.get() (1)
	.uri("https://example.com") (2)
	.retrieve() (3)
	.body<String>() (4)

println(result) (5)
1 設定 GET 請求
2 指定要連線的 URL
3 檢索響應
4 將響應轉換為字串
5 列印結果

透過 ResponseEntity 提供對響應狀態碼和響應頭的訪問

  • Java

  • Kotlin

ResponseEntity<String> result = restClient.get() (1)
	.uri("https://example.com") (1)
	.retrieve()
	.toEntity(String.class); (2)

System.out.println("Response status: " + result.getStatusCode()); (3)
System.out.println("Response headers: " + result.getHeaders()); (3)
System.out.println("Contents: " + result.getBody()); (3)
1 為指定的 URL 設定 GET 請求
2 將響應轉換為 ResponseEntity
3 列印結果
val result = restClient.get() (1)
	.uri("https://example.com") (1)
	.retrieve()
	.toEntity<String>() (2)

println("Response status: " + result.statusCode) (3)
println("Response headers: " + result.headers) (3)
println("Contents: " + result.body) (3)
1 為指定的 URL 設定 GET 請求
2 將響應轉換為 ResponseEntity
3 列印結果

RestClient 可以使用 Jackson 庫將 JSON 轉換為物件。請注意此示例中 URI 變數的用法,以及 Accept 頭已設定為 JSON。

  • Java

  • Kotlin

int id = ...;
Pet pet = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id) (1)
	.accept(APPLICATION_JSON) (2)
	.retrieve()
	.body(Pet.class); (3)
1 使用 URI 變數
2 Accept 頭設定為 application/json
3 將 JSON 響應轉換為 Pet 領域物件
val id = ...
val pet = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id) (1)
	.accept(APPLICATION_JSON) (2)
	.retrieve()
	.body<Pet>() (3)
1 使用 URI 變數
2 Accept 頭設定為 application/json
3 將 JSON 響應轉換為 Pet 領域物件

在下一個示例中,RestClient 用於執行包含 JSON 的 POST 請求,該請求同樣使用 Jackson 進行轉換。

  • Java

  • Kotlin

Pet pet = ... (1)
ResponseEntity<Void> response = restClient.post() (2)
	.uri("https://petclinic.example.com/pets/new") (2)
	.contentType(APPLICATION_JSON) (3)
	.body(pet) (4)
	.retrieve()
	.toBodilessEntity(); (5)
1 建立 Pet 領域物件
2 設定 POST 請求和要連線的 URL
3 Content-Type 頭設定為 application/json
4 使用 pet 作為請求體
5 將響應轉換為不帶正文的響應實體。
val pet: Pet = ... (1)
val response = restClient.post() (2)
	.uri("https://petclinic.example.com/pets/new") (2)
	.contentType(APPLICATION_JSON) (3)
	.body(pet) (4)
	.retrieve()
	.toBodilessEntity() (5)
1 建立 Pet 領域物件
2 設定 POST 請求和要連線的 URL
3 Content-Type 頭設定為 application/json
4 使用 pet 作為請求體
5 將響應轉換為不帶正文的響應實體。

錯誤處理

預設情況下,當檢索到 4xx 或 5xx 狀態碼的響應時,RestClient 會丟擲 RestClientException 的子類。此行為可以使用 onStatus 覆蓋。

  • Java

  • Kotlin

String result = restClient.get() (1)
	.uri("https://example.com/this-url-does-not-exist") (1)
	.retrieve()
	.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { (2)
		throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); (3)
	})
	.body(String.class);
1 為返回 404 狀態碼的 URL 建立 GET 請求
2 為所有 4xx 狀態碼設定狀態處理器
3 丟擲自定義異常
val result = restClient.get() (1)
	.uri("https://example.com/this-url-does-not-exist") (1)
	.retrieve()
	.onStatus(HttpStatusCode::is4xxClientError) { _, response -> (2)
		throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } (3)
	.body<String>()
1 為返回 404 狀態碼的 URL 建立 GET 請求
2 為所有 4xx 狀態碼設定狀態處理器
3 丟擲自定義異常

交換

對於更高階的場景,RestClient 透過 exchange() 方法提供對底層 HTTP 請求和響應的訪問,該方法可以替代 retrieve() 使用。在使用 exchange() 時不會應用狀態處理程式,因為 exchange 函式已經提供了對完整響應的訪問,允許您執行任何必要的錯誤處理。

  • Java

  • Kotlin

Pet result = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id)
	.accept(APPLICATION_JSON)
	.exchange((request, response) -> { (1)
		if (response.getStatusCode().is4xxClientError()) { (2)
			throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); (2)
		}
		else {
			Pet pet = convertResponse(response); (3)
			return pet;
		}
	});
1 exchange 提供請求和響應
2 當響應狀態碼為 4xx 時丟擲異常
3 將響應轉換為 Pet 領域物件
val result = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id)
	.accept(MediaType.APPLICATION_JSON)
	.exchange { request, response -> (1)
		if (response.getStatusCode().is4xxClientError()) { (2)
			throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) (2)
		} else {
			val pet: Pet = convertResponse(response) (3)
			pet
		}
	}
1 exchange 提供請求和響應
2 當響應狀態碼為 4xx 時丟擲異常
3 將響應轉換為 Pet 領域物件

HTTP 訊息轉換

Jackson JSON 檢視

要僅序列化物件屬性的子集,您可以指定 Jackson JSON 檢視,如以下示例所示

MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);

ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
	.contentType(APPLICATION_JSON)
	.body(value)
	.retrieve()
	.toBodilessEntity();

多部分

要傳送多部分資料,您需要提供一個 MultiValueMap<String, Object>,其值可以是用於部分內容的 Object、用於檔案部分的 Resource 或用於包含頭的部分內容的 HttpEntity。例如

MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();

parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));

// send using RestClient.post or RestTemplate.postForEntity

在大多數情況下,您無需為每個部分指定 Content-Type。內容型別會根據所選的 HttpMessageConverter 自動確定以進行序列化,或者在 Resource 的情況下,根據副檔名確定。如有必要,您可以使用 HttpEntity 包裝器明確提供 MediaType

一旦 MultiValueMap 準備就緒,您可以將其用作 POST 請求的主體,使用 RestClient.post().body(parts)(或 RestTemplate.postForObject)。

如果 MultiValueMap 包含至少一個非 String 值,則 FormHttpMessageConverter 會將 Content-Type 設定為 multipart/form-data。如果 MultiValueMap 具有 String 值,則 Content-Type 預設為 application/x-www-form-urlencoded。如有必要,Content-Type 也可以顯式設定。

客戶端請求工廠

為了執行 HTTP 請求,RestClient 使用客戶端 HTTP 庫。這些庫透過 ClientRequestFactory 介面進行適配。有多種實現可用

  • JdkClientHttpRequestFactory 用於 Java 的 HttpClient

  • HttpComponentsClientHttpRequestFactory 用於 Apache HTTP Components HttpClient

  • JettyClientHttpRequestFactory 用於 Jetty 的 HttpClient

  • ReactorNettyClientRequestFactory 用於 Reactor Netty 的 HttpClient

  • SimpleClientHttpRequestFactory 作為簡單的預設值

如果在構建 RestClient 時未指定請求工廠,它將使用 Apache 或 Jetty HttpClient(如果它們在類路徑上可用)。否則,如果載入了 java.net.http 模組,它將使用 Java 的 HttpClient。最後,它將退回到簡單的預設值。

請注意,SimpleClientHttpRequestFactory 在訪問表示錯誤(例如 401)的響應狀態時可能會引發異常。如果這是一個問題,請使用任何替代的請求工廠。

WebClient

WebClient 是一個非阻塞的響應式客戶端,用於執行 HTTP 請求。它於 5.0 引入,提供 RestTemplate 的替代方案,支援同步、非同步和流式傳輸場景。

WebClient 支援以下功能

  • 非阻塞 I/O

  • 響應式流背壓

  • 以更少的硬體資源實現高併發

  • 利用 Java 8 Lambda 的函式式、流式 API

  • 同步和非同步互動

  • 向上遊或下游伺服器進行流式傳輸

有關更多詳細資訊,請參閱 WebClient

RestTemplate

RestTemplate 以經典的 Spring Template 類的形式提供了對 HTTP 客戶端庫的高階 API。它公開了以下幾組過載方法

從 Spring Framework 7.0 開始,RestTemplate 已被棄用,轉而使用 RestClient,並將在未來版本中移除。請使用 “遷移到 RestClient” 指南。對於非同步和流式傳輸場景,請考慮響應式 WebClient
表 1. RestTemplate 方法
方法組 描述

getForObject

透過 GET 檢索表示。

getForEntity

透過 GET 檢索 ResponseEntity(即狀態、頭部和正文)。

headForHeaders

透過 HEAD 檢索資源的所有頭部。

postForLocation

透過 POST 建立新資源並返回響應中的 Location 頭部。

postForObject

透過 POST 建立新資源並返回響應中的表示。

postForEntity

透過 POST 建立新資源並返回響應中的表示。

put

透過 PUT 建立或更新資源。

patchForObject

透過 PATCH 更新資源並返回響應中的表示。請注意,JDK HttpURLConnection 不支援 PATCH,但 Apache HttpComponents 和其他支援。

delete

透過 DELETE 刪除指定 URI 處的資源。

optionsForAllow

透過 ALLOW 檢索資源允許的 HTTP 方法。

exchange

是上述方法的更通用(且不那麼主觀)的版本,在需要時提供額外的靈活性。它接受 RequestEntity(包括 HTTP 方法、URL、頭部和作為輸入的正文)並返回 ResponseEntity

這些方法允許使用 ParameterizedTypeReference 而不是 Class 來指定具有泛型的響應型別。

execute

執行請求的最通用方式,透過回撥介面完全控制請求準備和響應提取。

初始化

RestTemplate 使用與 RestClient 相同的 HTTP 庫抽象。預設情況下,它使用 SimpleClientHttpRequestFactory,但這可以透過建構函式更改。請參閱 客戶端請求工廠

RestTemplate 可以進行可觀測性檢測,以生成指標和跟蹤。請參閱 RestTemplate 可觀測性支援 部分。

正文

傳入和傳出 RestTemplate 方法的物件在 HttpMessageConverter 的幫助下轉換為 HTTP 訊息,請參閱 HTTP 訊息轉換

遷移到 RestClient

應用程式可以逐步採用 RestClient,首先關注 API 使用,然後關注基礎設施設定。您可以考慮以下步驟

  1. 從現有的 RestTemplate 例項建立一個或多個 RestClient,例如:RestClient restClient = RestClient.create(restTemplate)。逐步替換應用程式中元件的 RestTemplate 用法,首先關注發出請求。請參閱下表中的 API 等效項。

  2. 一旦所有客戶端請求都透過 RestClient 例項,您現在可以使用 RestClient.Builder 來複制您現有的 RestTemplate 例項建立。由於 RestTemplateRestClient 共享相同的基礎設施,您可以在設定中重用自定義的 ClientHttpRequestFactoryClientHttpRequestInterceptor。請參閱 RestClient 構建器 API

如果在類路徑上沒有其他庫可用,RestClient 將選擇由現代 JDK HttpClient 支援的 JdkClientHttpRequestFactory,而 RestTemplate 將選擇使用 HttpURLConnectionSimpleClientHttpRequestFactory。這可以解釋 HTTP 級別執行時微妙的行為差異。

下表顯示了 RestTemplate 方法的 RestClient 等效項。

表 2. RestTemplate 方法的 RestClient 等效項
RestTemplate 方法 RestClient 等效項

getForObject(String, Class, Object…​)

get() .uri(String, Object…​) .retrieve() .body(Class)

getForObject(String, Class, Map)

get() .uri(String, Map) .retrieve() .body(Class)

getForObject(URI, Class)

get() .uri(URI) .retrieve() .body(Class)

getForEntity(String, Class, Object…​)

get() .uri(String, Object…​) .retrieve() .toEntity(Class)

getForEntity(String, Class, Map)

get() .uri(String, Map) .retrieve() .toEntity(Class)

getForEntity(URI, Class)

get() .uri(URI) .retrieve() .toEntity(Class)

headForHeaders(String, Object…​)

head() .uri(String, Object…​) .retrieve() .toBodilessEntity() .getHeaders()

headForHeaders(String, Map)

head() .uri(String, Map) .retrieve() .toBodilessEntity() .getHeaders()

headForHeaders(URI)

head() .uri(URI) .retrieve() .toBodilessEntity() .getHeaders()

postForLocation(String, Object, Object…​)

post() .uri(String, Object…​) .body(Object).retrieve() .toBodilessEntity() .getLocation()

postForLocation(String, Object, Map)

post() .uri(String, Map) .body(Object) .retrieve() .toBodilessEntity() .getLocation()

postForLocation(URI, Object)

post() .uri(URI) .body(Object) .retrieve() .toBodilessEntity() .getLocation()

postForObject(String, Object, Class, Object…​)

post() .uri(String, Object…​) .body(Object) .retrieve() .body(Class)

postForObject(String, Object, Class, Map)

post() .uri(String, Map) .body(Object) .retrieve() .body(Class)

postForObject(URI, Object, Class)

post() .uri(URI) .body(Object) .retrieve() .body(Class)

postForEntity(String, Object, Class, Object…​)

post() .uri(String, Object…​) .body(Object) .retrieve() .toEntity(Class)

postForEntity(String, Object, Class, Map)

post() .uri(String, Map) .body(Object) .retrieve() .toEntity(Class)

postForEntity(URI, Object, Class)

post() .uri(URI) .body(Object) .retrieve() .toEntity(Class)

put(String, Object, Object…​)

put() .uri(String, Object…​) .body(Object) .retrieve() .toBodilessEntity()

put(String, Object, Map)

put() .uri(String, Map) .body(Object) .retrieve() .toBodilessEntity()

put(URI, Object)

put() .uri(URI) .body(Object) .retrieve() .toBodilessEntity()

patchForObject(String, Object, Class, Object…​)

patch() .uri(String, Object…​) .body(Object) .retrieve() .body(Class)

patchForObject(String, Object, Class, Map)

patch() .uri(String, Map) .body(Object) .retrieve() .body(Class)

patchForObject(URI, Object, Class)

patch() .uri(URI) .body(Object) .retrieve() .body(Class)

delete(String, Object…​)

delete() .uri(String, Object…​) .retrieve() .toBodilessEntity()

delete(String, Map)

delete() .uri(String, Map) .retrieve() .toBodilessEntity()

delete(URI)

delete() .uri(URI) .retrieve() .toBodilessEntity()

optionsForAllow(String, Object…​)

options() .uri(String, Object…​) .retrieve() .toBodilessEntity() .getAllow()

optionsForAllow(String, Map)

options() .uri(String, Map) .retrieve() .toBodilessEntity() .getAllow()

optionsForAllow(URI)

options() .uri(URI) .retrieve() .toBodilessEntity() .getAllow()

exchange(String, HttpMethod, HttpEntity, Class, Object…​)

method(HttpMethod) .uri(String, Object…​) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]

exchange(String, HttpMethod, HttpEntity, Class, Map)

method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]

exchange(URI, HttpMethod, HttpEntity, Class)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]

exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Object…​)

method(HttpMethod) .uri(String, Object…​) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]

exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Map)

method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]

exchange(URI, HttpMethod, HttpEntity, ParameterizedTypeReference)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]

exchange(RequestEntity, Class)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [2]

exchange(RequestEntity, ParameterizedTypeReference)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [2]

execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object…​)

method(HttpMethod) .uri(String, Object…​) .exchange(ExchangeFunction)

execute(String, HttpMethod, RequestCallback, ResponseExtractor, Map)

method(HttpMethod) .uri(String, Map) .exchange(ExchangeFunction)

execute(URI, HttpMethod, RequestCallback, ResponseExtractor)

method(HttpMethod) .uri(URI) .exchange(ExchangeFunction)

RestClientRestTemplate 例項在丟擲異常(RestClientException 型別位於層次結構頂部)時具有相同的行為。當 RestTemplate 始終針對“4xx”響應狀態丟擲 HttpClientErrorException 時,RestClient 允許使用自定義的 “狀態處理程式” 提供更大的靈活性。

HTTP 服務客戶端

您可以將 HTTP 服務定義為具有 @HttpExchange 方法的 Java 介面,並使用 HttpServiceProxyFactory 從中建立一個客戶端代理,透過 RestClientWebClientRestTemplate 進行遠端 HTTP 訪問。在伺服器端,@Controller 類可以實現相同的介面,使用 @HttpExchange 控制器方法來處理請求。

首先,建立 Java 介面

public interface RepositoryService {

	@GetExchange("/repos/{owner}/{repo}")
	Repository getRepository(@PathVariable String owner, @PathVariable String repo);

	// more HTTP exchange methods...

}

(可選)在型別級別使用 @HttpExchange 宣告所有方法的通用屬性

@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
public interface RepositoryService {

	@GetExchange
	Repository getRepository(@PathVariable String owner, @PathVariable String repo);

	@PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
	void updateRepository(@PathVariable String owner, @PathVariable String repo,
			@RequestParam String name, @RequestParam String description, @RequestParam String homepage);

}

接下來,配置客戶端並建立 HttpServiceProxyFactory

// Using RestClient...

RestClient restClient = RestClient.create("...");
RestClientAdapter adapter = RestClientAdapter.create(restClient);

// or WebClient...

WebClient webClient = WebClient.create("...");
WebClientAdapter adapter = WebClientAdapter.create(webClient);

// or RestTemplate...

RestTemplate restTemplate = new RestTemplate();
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);

HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

現在,您可以建立客戶端代理了

RepositoryService service = factory.createClient(RepositoryService.class);
// Use service methods for remote calls...

HTTP 服務客戶端是一種強大而富有表現力的 HTTP 遠端訪問選擇。它允許一個團隊掌握 REST API 的工作方式、哪些部分與客戶端應用程式相關、建立哪些輸入和輸出型別、需要哪些端點方法簽名、擁有哪些 Javadoc 等知識。生成的 Java API 易於理解且可直接使用。

方法引數

@HttpExchange 方法支援具有以下輸入的靈活方法簽名

方法引數 描述

URI

動態設定請求的 URL,覆蓋註解的 url 屬性。

UriBuilderFactory

提供 UriBuilderFactory 以擴充套件 URI 模板和 URI 變數。實際上,它替換了底層客戶端的 UriBuilderFactory(及其基本 URL)。

HttpMethod

動態設定請求的 HTTP 方法,覆蓋註解的 method 屬性

@RequestHeader

新增一個請求頭或多個請求頭。引數可以是單個值、Collection<?> 型別的值、Map<String, ?>MultiValueMap<String, ?>。支援非字串值的型別轉換。請求頭值是新增的,不會覆蓋已經新增的請求頭值。

@PathVariable

新增一個變數以擴充套件請求 URL 中的佔位符。引數可以是包含多個變數的 Map<String, ?>,也可以是單個值。支援非字串值的型別轉換。

@RequestAttribute

提供一個 Object 作為請求屬性。僅受 RestClientWebClient 支援。

@RequestBody

提供請求體,可以是一個待序列化的物件,也可以是響應式流 Publisher(例如 MonoFlux)或透過配置的 ReactiveAdapterRegistry 支援的任何其他非同步型別。

@RequestParam

新增一個或多個請求引數。引數可以是包含多個引數的 Map<String, ?>MultiValueMap<String, ?>,也可以是 Collection<?> 型別的值或單個值。支援非字串值的型別轉換。

"content-type" 設定為 "application/x-www-form-urlencoded" 時,請求引數在請求體中編碼。否則,它們作為 URL 查詢引數新增。

@RequestPart

新增請求部分,可以是字串(表單欄位)、Resource(檔案部分)、物件(要編碼的實體,例如 JSON)、HttpEntity(部分內容和頭部)、Spring Part,或上述任何一種的響應式流 Publisher

MultipartFile

MultipartFile 新增請求部分,通常在 Spring MVC 控制器中使用,它代表一個上傳的檔案。

@CookieValue

新增一個或多個 Cookie。引數可以是包含多個 Cookie 的 Map<String, ?>MultiValueMap<String, ?>,也可以是 Collection<?> 型別的值或單個值。支援非字串值的型別轉換。

方法引數不能為 null,除非 required 屬性(如果引數註解可用)設定為 false,或者引數被標記為可選,這由 MethodParameter#isOptional 確定。

RestClientAdapter 額外支援 StreamingHttpOutputMessage.Body 型別的方法引數,允許透過寫入 OutputStream 傳送請求體。

自定義引數

您可以配置自定義的 HttpServiceArgumentResolver。下面的示例介面使用自定義的 Search 方法引數型別

  • Java

  • Kotlin

public interface RepositoryService {

	@GetExchange("/repos/search")
	List<Repository> searchRepository(Search search);

}
interface RepositoryService {

	@GetExchange("/repos/search")
	fun searchRepository(search: Search): List<Repository>

}

自定義引數解析器可以這樣實現

  • Java

  • Kotlin

static class SearchQueryArgumentResolver implements HttpServiceArgumentResolver {
	@Override
	public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
		if (parameter.getParameterType().equals(Search.class)) {
			Search search = (Search) argument;
			requestValues.addRequestParameter("owner", search.owner());
			requestValues.addRequestParameter("language", search.language());
			requestValues.addRequestParameter("query", search.query());
			return true;
		}
		return false;
	}
}
class SearchQueryArgumentResolver : HttpServiceArgumentResolver {
	override fun resolve(
		argument: Any?,
		parameter: MethodParameter,
		requestValues: HttpRequestValues.Builder
	): Boolean {
		if (parameter.getParameterType() == Search::class.java) {
			val search = argument as Search
			requestValues.addRequestParameter("owner", search.owner)
				.addRequestParameter("language", search.language)
				.addRequestParameter("query", search.query)
			return true
		}
		return false
	}
}

配置自定義引數解析器

  • Java

  • Kotlin

RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory
		.builderFor(adapter)
		.customArgumentResolver(new SearchQueryArgumentResolver())
		.build();
RepositoryService repositoryService = factory.createClient(RepositoryService.class);

Search search = Search.create()
		.owner("spring-projects")
		.language("java")
		.query("rest")
		.build();
List<Repository> repositories = repositoryService.searchRepository(search);
val restClient = RestClient.builder().baseUrl("https://api.github.com/").build()
val adapter = RestClientAdapter.create(restClient)
val factory = HttpServiceProxyFactory
	.builderFor(adapter)
	.customArgumentResolver(SearchQueryArgumentResolver())
	.build()
val repositoryService = factory.createClient<RepositoryService>(RepositoryService::class.java)

val search = Search(owner = "spring-projects", language = "java", query = "rest")
val repositories = repositoryService.searchRepository(search)
預設情況下,RequestEntity 不支援作為方法引數,而是鼓勵對請求的各個部分使用更細粒度的方法引數。

返回值

支援的返回值取決於底層客戶端。

適配到 HttpExchangeAdapter 的客戶端(例如 RestClientRestTemplate)支援同步返回值

方法返回值 描述

void

執行給定的請求。

HttpHeaders

執行給定的請求並返回響應頭。

<T>

執行給定的請求並將響應內容解碼為宣告的返回型別。

ResponseEntity<Void>

執行給定的請求並返回包含狀態和頭部的 ResponseEntity

ResponseEntity<T>

執行給定的請求,將響應內容解碼為宣告的返回型別,並返回包含狀態、頭部和解碼後的正文的 ResponseEntity

適配到 ReactorHttpExchangeAdapter 的客戶端(例如 WebClient)支援上述所有功能以及響應式變體。下表顯示了 Reactor 型別,但您也可以使用透過 ReactiveAdapterRegistry 支援的其他響應式型別

方法返回值 描述

Mono<Void>

執行給定請求,並釋放響應內容(如果有)。

Mono<HttpHeaders>

執行給定請求,釋放響應內容(如果有),並返回響應頭。

Mono<T>

執行給定的請求並將響應內容解碼為宣告的返回型別。

Flux<T>

執行給定的請求並將響應內容解碼為宣告元素型別的流。

Mono<ResponseEntity<Void>>

執行給定請求,並釋放響應內容(如果有),並返回包含狀態和頭部的 ResponseEntity

Mono<ResponseEntity<T>>

執行給定的請求,將響應內容解碼為宣告的返回型別,並返回包含狀態、頭部和解碼後的正文的 ResponseEntity

Mono<ResponseEntity<Flux<T>>

執行給定請求,將響應內容解碼為宣告元素型別的流,並返回包含狀態、頭部和解碼後的響應體流的 ResponseEntity

預設情況下,使用 ReactorHttpExchangeAdapter 的同步返回值的超時時間取決於底層 HTTP 客戶端的配置方式。您也可以在介面卡級別設定 blockTimeout 值,但我們建議依賴底層 HTTP 客戶端的超時設定,該客戶端在較低級別操作並提供更多控制。

RestClientAdapter 額外支援 InputStreamResponseEntity<InputStream> 型別的返回值,提供對原始響應體內容的訪問。

錯誤處理

要自定義 HTTP 服務客戶端代理的錯誤處理,您可以根據需要配置底層客戶端。預設情況下,客戶端會對 4xx 和 5xx HTTP 狀態碼丟擲異常。要自定義此行為,請註冊一個適用於透過客戶端執行的所有響應的響應狀態處理程式,如下所示

// For RestClient
RestClient restClient = RestClient.builder()
		.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...)
		.build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);

// or for WebClient...
WebClient webClient = WebClient.builder()
		.defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
		.build();
WebClientAdapter adapter = WebClientAdapter.create(webClient);

// or for RestTemplate...
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(myErrorHandler);

RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);

HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

有關更多詳細資訊和選項(例如抑制錯誤狀態碼),請參閱每個客戶端的參考文件,以及 RestClient.BuilderWebClient.Builder 中的 defaultStatusHandler 的 Javadoc,以及 RestTemplatesetErrorHandler

裝飾介面卡

HttpExchangeAdapterReactorHttpExchangeAdapter 是將 HTTP 介面客戶端基礎設施與呼叫底層客戶端的細節解耦的契約。有針對 RestClientWebClientRestTemplate 的介面卡實現。

有時,透過在 HttpServiceProxyFactory.Builder 中配置裝飾器來攔截客戶端呼叫可能很有用。例如,您可以應用內建裝飾器來抑制 404 異常並返回帶有 NOT_FOUNDnull 正文的 ResponseEntity

// For RestClient
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(restCqlientAdapter)
		.exchangeAdapterDecorator(NotFoundRestClientAdapterDecorator::new)
		.build();

// or for WebClient...
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builderFor(webClientAdapter)
		.exchangeAdapterDecorator(NotFoundWebClientAdapterDecorator::new)
		.build();

HTTP 服務組

使用 HttpServiceProxyFactory 建立客戶端代理是微不足道的,但將它們宣告為 bean 會導致重複配置。您可能還有多個目標主機,因此需要配置多個客戶端,甚至需要建立更多的客戶端代理 bean。

為了更輕鬆地大規模使用介面客戶端,Spring Framework 提供了專門的配置支援。它允許應用程式專注於按組識別 HTTP 服務,併為每個組自定義客戶端,而框架透明地建立客戶端代理登錄檔,並將每個代理宣告為 bean。

HTTP 服務組簡單來說就是一組共享相同客戶端設定和 HttpServiceProxyFactory 例項來建立代理的介面。通常,這意味著每個主機一個組,但如果底層客戶端需要以不同方式配置,則同一目標主機可以有多個組。

宣告 HTTP 服務組的一種方式是透過 @Configuration 類中的 @ImportHttpServices 註解,如下所示

@Configuration
@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) (1)
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) (2)
public class ClientConfig {
}
1 手動列出“echo”組的介面
2 在一個基礎包下檢測“greeting”組的介面

也可以透過建立 HTTP 服務註冊器然後匯入它來以程式設計方式宣告組

public class MyHttpServiceRegistrar extends AbstractHttpServiceRegistrar { (1)

	@Override
	protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
		registry.forGroup("echo").register(EchoServiceA.class, EchoServiceB.class); (2)
		registry.forGroup("greeting").detectInBasePackages(GreetServiceA.class); (3)
	}
}

@Configuration
@Import(MyHttpServiceRegistrar.class) (4)
public class ClientConfig {
}
1 建立 AbstractHttpServiceRegistrar 的擴充套件類
2 手動列出“echo”組的介面
3 在一個基礎包下檢測“greeting”組的介面
4 匯入註冊器
您可以混合搭配 @ImportHttpService 註解和程式設計註冊器,並且可以將匯入分散到多個配置類中。所有匯入都協同作用於相同的共享 HttpServiceProxyRegistry 例項。

宣告 HTTP 服務組後,新增 HttpServiceGroupConfigurer bean 以自定義每個組的客戶端。例如

@Configuration
@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class})
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class)
public class ClientConfig {

	@Bean
	public RestClientHttpServiceGroupConfigurer groupConfigurer() {
		return groups -> {
			// configure client for group "echo"
			groups.filterByName("echo").forEachClient((group, clientBuilder) -> ...);

			// configure the clients for all groups
			groups.forEachClient((group, clientBuilder) -> ...);

			// configure client and proxy factory for each group
			groups.forEachGroup((group, clientBuilder, factoryBuilder) -> ...);
		};
	}
}
Spring Boot 使用 HttpServiceGroupConfigurer 新增對 HTTP 服務組的客戶端屬性支援,Spring Security 新增 OAuth 支援,Spring Cloud 新增負載均衡。

因此,每個客戶端代理都可作為 bean 使用,您可以方便地按型別自動裝配

@RestController
public class EchoController {

	private final EchoService echoService;

	public EchoController(EchoService echoService) {
		this.echoService = echoService;
	}

	// ...
}

但是,如果存在多個相同型別的客戶端代理,例如在多個組中的相同介面,則不存在該型別的唯一 bean,您不能僅按型別自動裝配。對於這種情況,您可以直接使用包含所有代理的 HttpServiceProxyRegistry,並按組獲取您需要的代理

@RestController
public class EchoController {

	private final EchoService echoService1;

	private final EchoService echoService2;

	public EchoController(HttpServiceProxyRegistry registry) {
		this.echoService1 = registry.getClient("echo1", EchoService.class); (1)
		this.echoService2 = registry.getClient("echo2", EchoService.class); (2)
	}

	// ...
}
1 訪問“echo1”組的 EchoService 客戶端代理
2 訪問“echo2”組的 EchoService 客戶端代理

1. HttpEntity 的頭部和正文必須透過 headers(Consumer<HttpHeaders>)body(Object) 提供給 RestClient
2. RequestEntity 的方法、URI、頭部和正文必須透過 method(HttpMethod)uri(URI)headers(Consumer<HttpHeaders>)body(Object) 提供給 RestClient
© . This site is unofficial and not affiliated with VMware.