REST 客户端

Spring Framework 提供了以下选项来调用 REST 端点:

  • RestClient — 具有流畅 API 的同步客户端

  • WebClient — 具有流畅 API 的非阻塞、响应式客户端

  • RestTemplate — 具有模板方法 API 的同步客户端

  • 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
String result = restClient.get() [id="CO1-1"]1
	.uri("https://example.com") [id="CO1-2"]2
	.retrieve() [id="CO1-3"]3
	.body(String.class); [id="CO1-4"]4

System.out.println(result); [id="CO1-5"]5
<1>  设置 GET 请求
<1>  指定要连接的 URL
<1>  检索响应
<1>  将响应转换为字符串
<1>  打印结果
Kotlin
val result= restClient.get() [id="CO2-1"]1
	.uri("https://example.com") [id="CO2-2"]2
	.retrieve() [id="CO2-3"]3
	.body<String>() [id="CO2-4"]4

println(result) [id="CO2-5"]5
<1>  设置 GET 请求
<1>  指定要连接的 URL
<1>  检索响应
<1>  将响应转换为字符串
<1>  打印结果

通过 ResponseEntity 提供对响应状态码和头的访问:

Java
ResponseEntity<String> result = restClient.get() [id="CO3-1"]1
	.uri("https://example.com") [id="CO3-2"]1
	.retrieve()
	.toEntity(String.class); [id="CO3-3"]2

System.out.println("Response status: " + result.getStatusCode()); [id="CO3-4"]3
System.out.println("Response headers: " + result.getHeaders()); [id="CO3-5"]3
System.out.println("Contents: " + result.getBody()); [id="CO3-6"]3
<1>  为指定的 URL 设置 GET 请求
<1>  将响应转换为 `ResponseEntity`
<1>  打印结果
Kotlin
val result = restClient.get() [id="CO4-1"]1
	.uri("https://example.com") [id="CO4-2"]1
	.retrieve()
	.toEntity<String>() [id="CO4-3"]2

println("Response status: " + result.statusCode) [id="CO4-4"]3
println("Response headers: " + result.headers) [id="CO4-5"]3
println("Contents: " + result.body) [id="CO4-6"]3
<1>  为指定的 URL 设置 GET 请求
<1>  将响应转换为 `ResponseEntity`
<1>  打印结果

RestClient 可以使用 Jackson 库将 JSON 转换为对象。请注意此示例中 URI 变量的用法,以及 Accept 头设置为 JSON。

Java
int id = ...;
Pet pet = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id) [id="CO5-1"]1
	.accept(APPLICATION_JSON) [id="CO5-2"]2
	.retrieve()
	.body(Pet.class); [id="CO5-3"]3
<1>  使用 URI 变量
<1>  将 `Accept` 头设置为 `application/json`
<1>  将 JSON 响应转换为 `Pet` 领域对象
Kotlin
val id = ...
val pet = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id) [id="CO6-1"]1
	.accept(APPLICATION_JSON) [id="CO6-2"]2
	.retrieve()
	.body<Pet>() [id="CO6-3"]3
<1>  使用 URI 变量
<1>  将 `Accept` 头设置为 `application/json`
<1>  将 JSON 响应转换为 `Pet` 领域对象

在下一个示例中,RestClient 用于执行包含 JSON 的 POST 请求,该 JSON 再次使用 Jackson 进行转换。

Java
Pet pet = ... [id="CO7-1"]1
ResponseEntity<Void> response = restClient.post() [id="CO7-2"]2
	.uri("https://petclinic.example.com/pets/new") [id="CO7-3"]2
	.contentType(APPLICATION_JSON) [id="CO7-4"]3
	.body(pet) [id="CO7-5"]4
	.retrieve()
	.toBodilessEntity(); [id="CO7-6"]5
<1>  创建一个 `Pet` 领域对象
<1>  设置 POST 请求和要连接的 URL
<1>  将 `Content-Type` 头设置为 `application/json`
<1>  使用 `pet` 作为请求体
<1>  将响应转换为不带体的响应实体。
Kotlin
val pet: Pet = ... [id="CO8-1"]1
val response = restClient.post() [id="CO8-2"]2
	.uri("https://petclinic.example.com/pets/new") [id="CO8-3"]2
	.contentType(APPLICATION_JSON) [id="CO8-4"]3
	.body(pet) [id="CO8-5"]4
	.retrieve()
	.toBodilessEntity() [id="CO8-6"]5
<1>  创建一个 `Pet` 领域对象
<1>  设置 POST 请求和要连接的 URL
<1>  将 `Content-Type` 头设置为 `application/json`
<1>  使用 `pet` 作为请求体
<1>  将响应转换为不带体的响应实体。

错误处理

默认情况下,当检索到 4xx 或 5xx 状态码的响应时,RestClient 会抛出 RestClientException 的子类。此行为可以通过 onStatus 进行覆盖。

Java
String result = restClient.get() [id="CO9-1"]1
	.uri("https://example.com/this-url-does-not-exist") [id="CO9-2"]1
	.retrieve()
	.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { [id="CO9-3"]2
		throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); [id="CO9-4"]3
	})
	.body(String.class);
<1>  为返回 404 状态码的 URL 创建 GET 请求
<1>  为所有 4xx 状态码设置状态处理器
<1>  抛出自定义异常
Kotlin
val result = restClient.get() [id="CO10-1"]1
	.uri("https://example.com/this-url-does-not-exist") [id="CO10-2"]1
	.retrieve()
	.onStatus(HttpStatusCode::is4xxClientError) { _, response -> [id="CO10-3"]2
		throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } [id="CO10-4"]3
	.body<String>()
<1>  为返回 404 状态码的 URL 创建 GET 请求
<1>  为所有 4xx 状态码设置状态处理器
<1>  抛出自定义异常

交换

对于更高级的场景,RestClient 通过 exchange() 方法提供对底层 HTTP 请求和响应的访问,该方法可以替代 retrieve() 使用。当使用 exchange() 时,状态处理器不会被应用,因为交换函数已经提供了对完整响应的访问,允许您执行任何必要的错误处理。

Java
Pet result = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id)
	.accept(APPLICATION_JSON)
	.exchange((request, response) -> { [id="CO11-1"]1
		if (response.getStatusCode().is4xxClientError()) { [id="CO11-2"]2
			throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); [id="CO11-3"]2
		}
		else {
			Pet pet = convertResponse(response); [id="CO11-4"]3
			return pet;
		}
	});
<1>  `exchange` 提供请求和响应
<1>  当响应具有 4xx 状态码时抛出异常
<1>  将响应转换为 Pet 领域对象
Kotlin
val result = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id)
	.accept(MediaType.APPLICATION_JSON)
	.exchange { request, response -> [id="CO12-1"]1
		if (response.getStatusCode().is4xxClientError()) { [id="CO12-2"]2
			throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) [id="CO12-3"]2
		} else {
			val pet: Pet = convertResponse(response) [id="CO12-4"]3
			pet
		}
	}
<1>  `exchange` 提供请求和响应
<1>  当响应具有 4xx 状态码时抛出异常
<1>  将响应转换为 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();

Multipart

要发送 multipart 数据,您需要提供一个 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 模板类的形式,为 HTTP 客户端库提供了一个高级 API。它公开了以下几组重载方法:

RestClient 为同步 HTTP 访问提供了更现代的 API。对于异步和流式传输场景,请考虑响应式 WebClient

Table 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 消息转换

RestTemplate 迁移到 RestClient

下表显示了 RestTemplate 方法的 RestClient 等效项。 它可用于从前者迁移到后者。

Table 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) [id="http-entity"][id="http-entity"][id="http-entity"]

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

method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) <<`HttpEntity` 头和正文必须通过 headers(Consumer<HttpHeaders>)body(Object) 提供给 RestClient。>>

exchange(URI, HttpMethod, HttpEntity, Class)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) <<`HttpEntity` 头和正文必须通过 headers(Consumer<HttpHeaders>)body(Object) 提供给 RestClient。>>

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

method(HttpMethod) .uri(String, Object…​) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) <<`HttpEntity` 头和正文必须通过 headers(Consumer<HttpHeaders>)body(Object) 提供给 RestClient。>>

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

method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) <<`HttpEntity` 头和正文必须通过 headers(Consumer<HttpHeaders>)body(Object) 提供给 RestClient。>>

exchange(URI, HttpMethod, HttpEntity, ParameterizedTypeReference)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) <<`HttpEntity` 头和正文必须通过 headers(Consumer<HttpHeaders>)body(Object) 提供给 RestClient。>>

exchange(RequestEntity, Class)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [id="request-entity"][id="request-entity"][id="request-entity"]

exchange(RequestEntity, ParameterizedTypeReference)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) <<`RequestEntity` 方法、URI、头和正文必须通过 method(HttpMethod)uri(URI)headers(Consumer<HttpHeaders>)body(Object) 提供给 RestClient。>>

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)

HTTP 接口客户端

您可以将 HTTP 服务定义为带有 @HttpExchange 方法的 Java 接口,并使用 HttpServiceProxyFactory 从中创建客户端代理,通过 HTTP 远程访问,使用 RestClientWebClientRestTemplate。在服务器端,@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...

方法参数

@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

提供请求体,可以是要序列化的 Object,也可以是 MonoFlux 或通过配置的 ReactiveAdapterRegistry 支持的任何其他异步类型等 Reactive Streams Publisher

@RequestParam

添加一个或多个请求参数。参数可以是包含多个参数的 Map<String, ?>MultiValueMap<String, ?>,也可以是 Collection<?> 类型的值集合,或 单个值。支持非字符串值的类型转换。

"content-type" 设置为 "application/x-www-form-urlencoded" 时,请求 参数在请求体中编码。否则,它们作为 URL 查询 参数添加。

@RequestPart

添加请求部分,可以是 String(表单字段)、Resource(文件部分)、 Object(要编码的实体,例如 JSON)、HttpEntity(部分内容和头)、 Spring Part,或上述任何一种的 Reactive Streams Publisher

MultipartFile

MultipartFile 添加请求部分,通常用于 Spring MVC 控制器中, 表示上传的文件。

@CookieValue

添加一个或多个 cookie。参数可以是包含多个 cookie 的 Map<String, ?>MultiValueMap<String, ?>,也可以是 Collection<?> 类型的值集合,或 单个值。支持非字符串值的类型转换。

方法参数不能为 null,除非 required 属性(在参数注解上可用)设置为 false,或者参数被标记为可选,由 MethodParameter#isOptional 确定。

RestClientAdapterStreamingHttpOutputMessage.Body 类型的方法参数提供额外支持,允许通过写入 OutputStream 发送请求体。

自定义参数

您可以配置自定义 HttpServiceArgumentResolver。下面的示例接口使用自定义 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 客户端的超时设置, 它在较低级别运行并提供更多控制。

RestClientAdapterInputStreamResponseEntity<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}) [id="CO13-1"][id="CO1-1"][id="CO13-1"](1)
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) [id="CO13-2"][id="CO1-2"][id="CO13-2"](2)
public class ClientConfig {
}
<1>  手动列出“echo”组的接口
<1>  在基本包下检测“greeting”组的接口

也可以通过创建 HTTP 服务注册器然后导入它来以编程方式声明组:

public class MyHttpServiceRegistrar extends AbstractHttpServiceRegistrar { [id="CO14-1"][id="CO1-3"][id="CO14-1"](1)

	@Override
	protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
		registry.forGroup("echo").register(EchoServiceA.class, EchoServiceB.class); [id="CO14-2"][id="CO1-4"][id="CO14-2"](2)
		registry.forGroup("greeting").detectInBasePackages(GreetServiceA.class); [id="CO14-3"][id="CO1-5"][id="CO14-3"](3)
	}
}

@Configuration
@Import(MyHttpServiceRegistrar.class) [id="CO14-4"][id="CO1-6"][id="CO14-4"](4)
public class ClientConfig {
}
<1>  创建 `AbstractHttpServiceRegistrar` 的扩展类
<1>  手动列出“echo”组的接口
<1>  在基本包下检测“greeting”组的接口
<1>  导入注册器

您可以混合使用 @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); [id="CO15-1"][id="CO1-7"][id="CO15-1"](1)
		this.echoService2 = registry.getClient("echo2", EchoService.class); [id="CO15-2"][id="CO1-8"][id="CO15-2"](2)
	}

	// ...
}
<1>  访问“echo1”组的 `EchoService` 客户端代理
<1>  访问“echo2”组的 `EchoService` 客户端代理