此版本仍在开发中,尚未稳定。如需最新的稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

REST 客户端

Spring 框架提供了以下几种调用 REST 端点的方式:spring-doc.cadn.net.cn

RestClient

RestClient 是一个同步HTTP客户端,提供了执行请求的流畅API。 它作为HTTP库的抽象层,处理HTTP请求和响应内容与更高层次Java对象之间的转换。spring-doc.cadn.net.cn

创建一个RestClient

RestClient 具有静态的 create 快捷方法。 它还提供了一个 builder() 以提供更多选项:spring-doc.cadn.net.cn

一旦创建,RestClient就可以在多个线程中安全使用。spring-doc.cadn.net.cn

以下展示了如何创建或构建一个RestClient:spring-doc.cadn.net.cn

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方法。可以使用便捷方法如GETPOSTPUT等,或是使用request()方法。spring-doc.cadn.net.cn

请求URL

接下来,使用 uri 方法指定请求URI。 这是可选的,如果您已经通过构建器配置了baseUrl,可以跳过这一步。 URL通常以 String 的形式指定,可包含可选的URI模板变量。 以下是如何执行请求的示例:spring-doc.cadn.net.cn

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

还可以使用函数进行更多控制,例如指定请求参数spring-doc.cadn.net.cn

字符串URL默认会被编码,但可以通过使用自定义的 uriBuilderFactory 构建客户端来更改此行为。 URL也可以通过函数或作为 java.net.URI 提供,这两种方式均不会被编码。 有关处理和编码URI的更多详细信息,请参见 URI链接spring-doc.cadn.net.cn

请求头和请求体

如有需要,可以通过添加请求头的方式操纵HTTP请求,使用header(String, String)headers(Consumer<HttpHeaders>或便捷方法accept(MediaType…​)acceptCharset(Charset…​)等进行操作。 对于可以包含正文的HTTP请求(POSTPUTPATCH),还有额外的方法可用:contentType(MediaType)contentLength(long)。 如果客户端配置了ApiVersionInserter,则可以为请求设置API版本。spring-doc.cadn.net.cn

请求体本身可以通过 body(Object) 来设置,其内部使用了 HTTP 消息转换。 或者,可以使用 ParameterizedTypeReference 来设置请求体,从而允许你使用泛型。 最后,可以将请求体设置为一个回调函数,该函数向 OutputStream 写入数据。spring-doc.cadn.net.cn

获取响应

请求设置完成后,可以通过在retrieve()之后链接方法调用来发送请求。 例如,可以使用retrieve().body(Class)retrieve().body(ParameterizedTypeReference)来访问响应体,适用于列表等参数化类型。 body方法将响应内容转换为多种类型——比如,字节可以转换为String,JSON可以使用Jackson转换为对象等(参见HTTP消息转换)。spring-doc.cadn.net.cn

响应也可以转换为一个ResponseEntity,从而可以访问响应头以及正文,使用retrieve().toEntity(Class)spring-doc.cadn.net.cn

调用 retrieve() 本身是一个无操作,返回 ResponseSpec。 应用程序必须在 ResponseSpec 上调用一个终端操作以产生任何副作用。 如果消费响应对您的用例没有兴趣,您可以使用 retrieve().toBodilessEntity()

此示例演示了如何使用 RestClient 执行简单的 GET 请求。spring-doc.cadn.net.cn

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 可以访问响应状态码和响应头:spring-doc.cadn.net.cn

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。spring-doc.cadn.net.cn

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请求,该JSON再次使用Jackson进行转换。spring-doc.cadn.net.cn

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 来重写。spring-doc.cadn.net.cn

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 函数已经提供了对完整响应的访问,允许你执行所需的任何错误处理。spring-doc.cadn.net.cn

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 视图,如下例所示:spring-doc.cadn.net.cn

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(用于带标题的部分内容)。例如:spring-doc.cadn.net.cn

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 包装器显式提供 MediaTypespring-doc.cadn.net.cn

一旦 MultiValueMap 准备就绪,你就可以将其用作 POST 请求的主体,使用 RestClient.post().body(parts)(或 RestTemplate.postForObject)。spring-doc.cadn.net.cn

如果 MultiValueMap 至少包含一个非 String 值,则 Content-Type 将由 FormHttpMessageConverter 设置为 multipart/form-data。 如果 MultiValueMap 具有 String 值,则 Content-Type 默认为 application/x-www-form-urlencoded。 如有必要,也可以显式设置 Content-Typespring-doc.cadn.net.cn

客户端请求工厂

为了执行HTTP请求,RestClient 使用了一个客户端HTTP库。 这些库通过 ClientRequestFactory 接口进行适配。 有多种实现可用:spring-doc.cadn.net.cn

如果在构建 RestClient 时未指定请求工厂,它将使用 Apache 或 Jetty 的 HttpClient(如果它们在类路径中可用)。 否则,如果已加载 java.net.http 模块,它将使用 Java 的 HttpClient。 最后,它将回退到简单的默认实现。spring-doc.cadn.net.cn

请注意,当访问表示错误的响应(例如,401)的状态时,SimpleClientHttpRequestFactory可能会引发异常。 如果这是个问题,请使用任何替代的请求工厂。

WebClient

WebClient 是一个用于执行 HTTP 请求的非阻塞、响应式客户端。它在 5.0 版本中引入,为 RestTemplate 提供了替代方案,支持同步、异步和流式场景。spring-doc.cadn.net.cn

WebClient 支持以下内容:spring-doc.cadn.net.cn

请参阅 WebClient 了解更多信息。spring-doc.cadn.net.cn

RestTemplate

RestTemplate 以经典的 Spring 模板类形式,在 HTTP 客户端库之上提供了高级 API。 它公开了以下几组重载方法:spring-doc.cadn.net.cn

自Spring Framework 7.0起,RestTemplate已被弃用,建议使用RestClient,并将在未来版本中移除,请参考“迁移到RestClient”的指南。 对于异步和流式处理场景,请考虑使用响应式的WebClient
表1. RestTemplate 方法
方法组 描述

getForObjectspring-doc.cadn.net.cn

通过 GET 方法获取表示。spring-doc.cadn.net.cn

getForEntityspring-doc.cadn.net.cn

通过使用GET方法获取一个<code>0</code>(即状态、标题和正文)。spring-doc.cadn.net.cn

headForHeadersspring-doc.cadn.net.cn

通过 HEAD 方法检索资源的所有标头。spring-doc.cadn.net.cn

postForLocationspring-doc.cadn.net.cn

通过使用 POST 创建新资源,并从响应中返回 Location 头信息。spring-doc.cadn.net.cn

postForObjectspring-doc.cadn.net.cn

通过使用 POST 创建一个新资源,并从响应中返回表示。spring-doc.cadn.net.cn

postForEntityspring-doc.cadn.net.cn

通过使用 POST 创建一个新资源,并从响应中返回表示。spring-doc.cadn.net.cn

putspring-doc.cadn.net.cn

通过 PUT 方法创建或更新资源。spring-doc.cadn.net.cn

patchForObjectspring-doc.cadn.net.cn

通过使用 PATCH 更新资源并从响应中返回表示。 请注意,JDK HttpURLConnection 不支持 PATCH,但 Apache HttpComponents 等其他组件支持。spring-doc.cadn.net.cn

deletespring-doc.cadn.net.cn

通过使用DELETE方法删除指定URI处的资源。spring-doc.cadn.net.cn

optionsForAllowspring-doc.cadn.net.cn

通过使用 ALLOW 来获取资源允许的 HTTP 方法。spring-doc.cadn.net.cn

exchangespring-doc.cadn.net.cn

前面方法的更通用(且更不具倾向性)版本,在需要时提供额外的灵活性。它接受一个 RequestEntity(包括HTTP方法、URL、头信息和正文作为输入),并返回一个 ResponseEntityspring-doc.cadn.net.cn

这些方法允许使用 ParameterizedTypeReference 而不是 Class 来指定带有泛型的响应类型。spring-doc.cadn.net.cn

executespring-doc.cadn.net.cn

执行请求的最通用方式,通过回调接口对请求准备和响应提取进行完全控制。spring-doc.cadn.net.cn

初始化

RestTemplate 使用与 RestClient 相同的HTTP库抽象。 默认情况下,它使用 SimpleClientHttpRequestFactory,但可以通过构造函数更改此设置。 请参阅 客户端请求工厂spring-doc.cadn.net.cn

RestTemplate 可以被用于可观测性,以生成指标和跟踪信息。 请参阅 RestTemplate 可观测性支持 部分。

正文

传递给 RestTemplate 方法以及从其返回的对象,会在 HttpMessageConverter 的帮助下转换为HTTP消息或从HTTP消息转换而来,参见 HTTP消息转换spring-doc.cadn.net.cn

迁移到 RestClient 版本

应用程序可以逐步采用 RestClient,首先集中于API的使用,然后是基础设施的设置。 您可以考虑以下步骤:spring-doc.cadn.net.cn

  1. 从现有RestTemplate实例创建一个或多个RestClient,例如:RestClient restClient = RestClient.create(restTemplate)。 逐步在您的应用程序中替换RestTemplate的使用,逐个组件地进行,首先专注于发出请求。 请参见下表以获取API对应项。spring-doc.cadn.net.cn

  2. 当所有客户端请求都通过RestClient实例传递后,您可以开始通过使用RestClient.Builder来复制现有 RestTemplate实例的创建。因为RestTemplateRestClient 共享相同的基础设施,您可以在设置中重用自定义的ClientHttpRequestFactoryClientHttpRequestInterceptor 。参见<code>7</code>构造器APIspring-doc.cadn.net.cn

如果类路径上没有其他库可用,RestClient 将选择由现代JDK HttpClient 支持的 JdkClientHttpRequestFactory,而 RestTemplate 会选用依赖 HttpURLConnectionSimpleClientHttpRequestFactory。这可以解释在HTTP层面运行时的细微行为差异。spring-doc.cadn.net.cn

下表展示了 RestClient 对应于 RestTemplate 方法的等价物。spring-doc.cadn.net.cn

表 2. RestTemplate 方法对应的 RestClient 等价物
RestTemplate 方法 RestClient 等效

getForObject(String, Class, Object…​)spring-doc.cadn.net.cn

get() .uri(String, Object…​) .retrieve() .body(Class)spring-doc.cadn.net.cn

getForObject(String, Class, Map)spring-doc.cadn.net.cn

get() .uri(String, Map) .retrieve() .body(Class)spring-doc.cadn.net.cn

getForObject(URI, Class)spring-doc.cadn.net.cn

get() .uri(URI) .retrieve() .body(Class)spring-doc.cadn.net.cn

getForEntity(String, Class, Object…​)spring-doc.cadn.net.cn

get() .uri(String, Object…​) .retrieve() .toEntity(Class)spring-doc.cadn.net.cn

getForEntity(String, Class, Map)spring-doc.cadn.net.cn

get() .uri(String, Map) .retrieve() .toEntity(Class)spring-doc.cadn.net.cn

getForEntity(URI, Class)spring-doc.cadn.net.cn

get() .uri(URI) .retrieve() .toEntity(Class)spring-doc.cadn.net.cn

headForHeaders(String, Object…​)spring-doc.cadn.net.cn

head() .uri(String, Object…​) .retrieve() .toBodilessEntity() .getHeaders()spring-doc.cadn.net.cn

headForHeaders(String, Map)spring-doc.cadn.net.cn

head() .uri(String, Map) .retrieve() .toBodilessEntity() .getHeaders()spring-doc.cadn.net.cn

headForHeaders(URI)spring-doc.cadn.net.cn

head() .uri(URI) .retrieve() .toBodilessEntity() .getHeaders()spring-doc.cadn.net.cn

postForLocation(String, Object, Object…​)spring-doc.cadn.net.cn

post() .uri(String, Object…​) .body(Object).retrieve() .toBodilessEntity() .getLocation()spring-doc.cadn.net.cn

postForLocation(String, Object, Map)spring-doc.cadn.net.cn

post() .uri(String, Map) .body(Object) .retrieve() .toBodilessEntity() .getLocation()spring-doc.cadn.net.cn

postForLocation(URI, Object)spring-doc.cadn.net.cn

post() .uri(URI) .body(Object) .retrieve() .toBodilessEntity() .getLocation()spring-doc.cadn.net.cn

postForObject(String, Object, Class, Object…​)spring-doc.cadn.net.cn

post() .uri(String, Object…​) .body(Object) .retrieve() .body(Class)spring-doc.cadn.net.cn

postForObject(String, Object, Class, Map)spring-doc.cadn.net.cn

post() .uri(String, Map) .body(Object) .retrieve() .body(Class)spring-doc.cadn.net.cn

postForObject(URI, Object, Class)spring-doc.cadn.net.cn

post() .uri(URI) .body(Object) .retrieve() .body(Class)spring-doc.cadn.net.cn

postForEntity(String, Object, Class, Object…​)spring-doc.cadn.net.cn

post() .uri(String, Object…​) .body(Object) .retrieve() .toEntity(Class)spring-doc.cadn.net.cn

postForEntity(String, Object, Class, Map)spring-doc.cadn.net.cn

post() .uri(String, Map) .body(Object) .retrieve() .toEntity(Class)spring-doc.cadn.net.cn

postForEntity(URI, Object, Class)spring-doc.cadn.net.cn

post() .uri(URI) .body(Object) .retrieve() .toEntity(Class)spring-doc.cadn.net.cn

put(String, Object, Object…​)spring-doc.cadn.net.cn

put() .uri(String, Object…​) .body(Object) .retrieve() .toBodilessEntity()spring-doc.cadn.net.cn

put(String, Object, Map)spring-doc.cadn.net.cn

put() .uri(String, Map) .body(Object) .retrieve() .toBodilessEntity()spring-doc.cadn.net.cn

put(URI, Object)spring-doc.cadn.net.cn

put() .uri(URI) .body(Object) .retrieve() .toBodilessEntity()spring-doc.cadn.net.cn

patchForObject(String, Object, Class, Object…​)spring-doc.cadn.net.cn

patch() .uri(String, Object…​) .body(Object) .retrieve() .body(Class)spring-doc.cadn.net.cn

patchForObject(String, Object, Class, Map)spring-doc.cadn.net.cn

patch() .uri(String, Map) .body(Object) .retrieve() .body(Class)spring-doc.cadn.net.cn

patchForObject(URI, Object, Class)spring-doc.cadn.net.cn

patch() .uri(URI) .body(Object) .retrieve() .body(Class)spring-doc.cadn.net.cn

delete(String, Object…​)spring-doc.cadn.net.cn

delete() .uri(String, Object…​) .retrieve() .toBodilessEntity()spring-doc.cadn.net.cn

delete(String, Map)spring-doc.cadn.net.cn

delete() .uri(String, Map) .retrieve() .toBodilessEntity()spring-doc.cadn.net.cn

delete(URI)spring-doc.cadn.net.cn

delete() .uri(URI) .retrieve() .toBodilessEntity()spring-doc.cadn.net.cn

optionsForAllow(String, Object…​)spring-doc.cadn.net.cn

options() .uri(String, Object…​) .retrieve() .toBodilessEntity() .getAllow()spring-doc.cadn.net.cn

optionsForAllow(String, Map)spring-doc.cadn.net.cn

options() .uri(String, Map) .retrieve() .toBodilessEntity() .getAllow()spring-doc.cadn.net.cn

optionsForAllow(URI)spring-doc.cadn.net.cn

options() .uri(URI) .retrieve() .toBodilessEntity() .getAllow()spring-doc.cadn.net.cn

exchange(String, HttpMethod, HttpEntity, Class, Object…​)spring-doc.cadn.net.cn

method(HttpMethod) .uri(String, Object…​) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]spring-doc.cadn.net.cn

exchange(String, HttpMethod, HttpEntity, Class, Map)spring-doc.cadn.net.cn

method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]spring-doc.cadn.net.cn

exchange(URI, HttpMethod, HttpEntity, Class)spring-doc.cadn.net.cn

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]spring-doc.cadn.net.cn

exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Object…​)spring-doc.cadn.net.cn

method(HttpMethod) .uri(String, Object…​) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]spring-doc.cadn.net.cn

exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Map)spring-doc.cadn.net.cn

method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]spring-doc.cadn.net.cn

exchange(URI, HttpMethod, HttpEntity, ParameterizedTypeReference)spring-doc.cadn.net.cn

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]spring-doc.cadn.net.cn

exchange(RequestEntity, Class)spring-doc.cadn.net.cn

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [2]spring-doc.cadn.net.cn

exchange(RequestEntity, ParameterizedTypeReference)spring-doc.cadn.net.cn

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [2]spring-doc.cadn.net.cn

execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object…​)spring-doc.cadn.net.cn

method(HttpMethod) .uri(String, Object…​) .exchange(ExchangeFunction)spring-doc.cadn.net.cn

execute(String, HttpMethod, RequestCallback, ResponseExtractor, Map)spring-doc.cadn.net.cn

method(HttpMethod) .uri(String, Map) .exchange(ExchangeFunction)spring-doc.cadn.net.cn

execute(URI, HttpMethod, RequestCallback, ResponseExtractor)spring-doc.cadn.net.cn

method(HttpMethod) .uri(URI) .exchange(ExchangeFunction)spring-doc.cadn.net.cn

RestClientRestTemplate 实例在抛出异常时表现出相同的行为(其中 RestClientException 类型位于层次结构的顶端)。 当 RestTemplate 一致地为 "4xx" 响应状态抛出 HttpClientErrorException 时, RestClient 则通过自定义的 "状态处理器" 提供了更大的灵活性。spring-doc.cadn.net.cn

HTTP 服务客户端

您可以将HTTP服务定义为一个具有@HttpExchange个方法的Java接口,并使用 HttpServiceProxyFactory从该接口创建客户端代理,以通过 RestClientWebClientRestTemplate进行HTTP远程访问。在服务器端,一个@Controller类 可以实现相同的接口来处理带有 @HttpExchange 控制器方法的请求。spring-doc.cadn.net.cn

首先,创建Java接口:spring-doc.cadn.net.cn

public interface RepositoryService {

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

	// more HTTP exchange methods...

}

可选地,在类型级别使用 @HttpExchange 声明所有方法的公共属性:spring-doc.cadn.net.cn

@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:spring-doc.cadn.net.cn

// 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();

现在,您可以创建客户端代理了:spring-doc.cadn.net.cn

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

HTTP服务客户端是进行HTTP远程访问的强大而富有表现力的选择。 它允许一个团队掌握REST API如何工作、哪些部分对客户端应用程序相关、需要创建什么输入和输出类型、需要什么端点方法签名、应具备哪些Javadoc等知识。最终形成的Java API既提供了指导也随时可以投入使用。spring-doc.cadn.net.cn

方法参数

@HttpExchange 个方法支持以下输入的灵活方法签名:spring-doc.cadn.net.cn

方法参数 描述

URIspring-doc.cadn.net.cn

动态设置请求的URL,覆盖注解的 url 属性。spring-doc.cadn.net.cn

UriBuilderFactoryspring-doc.cadn.net.cn

提供一个 UriBuilderFactory 用于展开URI模板和URI变量。 实际上,会替换底层客户端的 UriBuilderFactory(及其基础URL)。spring-doc.cadn.net.cn

HttpMethodspring-doc.cadn.net.cn

动态设置请求的HTTP方法,覆盖注解的 method 属性spring-doc.cadn.net.cn

@RequestHeaderspring-doc.cadn.net.cn

向请求中添加一个或多个头部信息。参数可以是单个值,也可以是Collection<?>个、Map<String, ?>个或MultiValueMap<String, ?>个值的集合。 非字符串类型的值支持类型转换。头部值会被添加而不覆盖已存在的头部值。spring-doc.cadn.net.cn

@PathVariablespring-doc.cadn.net.cn

添加一个变量以在请求URL中展开占位符。该参数可以是多个变量时的Map<String, ?>,也可以是单个值。支持非字符串值的类型转换。spring-doc.cadn.net.cn

@RequestAttributespring-doc.cadn.net.cn

向请求属性中添加时提供一个Object。仅受RestClientWebClient支持。spring-doc.cadn.net.cn

@RequestBodyspring-doc.cadn.net.cn

将请求的正文提供为要序列化的对象,或作为响应式流 Publisher(例如 MonoFlux),或通过配置的 ReactiveAdapterRegistry 支持的任何其他异步类型。spring-doc.cadn.net.cn

@RequestParamspring-doc.cadn.net.cn

添加一个请求参数或多个参数。该参数可以是多个参数的 Map<String, ?>MultiValueMap<String, ?>,值的 Collection<?>,或单个值。支持非字符串值的类型转换。spring-doc.cadn.net.cn

当将 "content-type" 设置为 "application/x-www-form-urlencoded" 时,请求参数会被编码到请求正文中。否则,它们将作为URL查询参数添加。spring-doc.cadn.net.cn

@RequestPartspring-doc.cadn.net.cn

添加一个请求部分,可以是字符串(表单字段)、Resource(文件部分), 对象(要编码的实体,例如,作为JSON)、HttpEntity(部分内容和头部), Spring的Part或 Reactive Streams 的Publisher,上述任意类型的流均可。spring-doc.cadn.net.cn

MultipartFilespring-doc.cadn.net.cn

MultipartFile 添加一个请求部分,通常在 Spring MVC 控制器中使用, 它表示一个上传的文件。spring-doc.cadn.net.cn

@CookieValuespring-doc.cadn.net.cn

添加一个或多个cookie。该参数可以是多个cookie的 Map<String, ?>MultiValueMap<String, ?>,一组值的 Collection<?>,或单个值。支持非字符串值的类型转换。spring-doc.cadn.net.cn

方法参数不能为null,除非该参数注解上的required属性(在可用时)设置为false,或者根据MethodParameter#isOptional确定该参数为可选。spring-doc.cadn.net.cn

RestClientAdapter 为类型为 StreamingHttpOutputMessage.Body 的方法参数提供了额外支持,该参数允许通过写入 OutputStream 来发送请求体。spring-doc.cadn.net.cn

自定义参数

您可以配置一个自定义的HttpServiceArgumentResolver。下面的示例接口使用了自定义的Search方法参数类型:spring-doc.cadn.net.cn

public interface RepositoryService {

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

}
interface RepositoryService {

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

}

一个自定义的参数解析器可以这样实现:spring-doc.cadn.net.cn

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
	}
}

要配置自定义参数解析器:spring-doc.cadn.net.cn

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不支持作为方法参数,而是鼓励使用更细粒度的方法参数来处理请求的各个部分。

返回值

支持的返回值取决于底层客户端。spring-doc.cadn.net.cn

适配 HttpExchangeAdapter 的客户端(如 RestClientRestTemplate) 支持同步返回值:spring-doc.cadn.net.cn

方法返回值 描述

voidspring-doc.cadn.net.cn

执行给定的请求。spring-doc.cadn.net.cn

HttpHeadersspring-doc.cadn.net.cn

执行给定的请求并返回响应头。spring-doc.cadn.net.cn

<T>spring-doc.cadn.net.cn

执行给定的请求,并将响应内容解码为声明的返回类型。spring-doc.cadn.net.cn

ResponseEntity<Void>spring-doc.cadn.net.cn

执行给定的请求,并返回一个包含状态和头部信息的 ResponseEntityspring-doc.cadn.net.cn

ResponseEntity<T>spring-doc.cadn.net.cn

执行给定的请求,将响应内容解码为声明的返回类型,并返回一个包含状态、头部和解码后主体的 ResponseEntityspring-doc.cadn.net.cn

适应了ReactorHttpExchangeAdapter的客户端,如WebClient,支持上述所有功能以及响应式变体。下表展示了Reactor类型,但您也可以使用通过ReactiveAdapterRegistry支持的其他响应式类型:spring-doc.cadn.net.cn

方法返回值 描述

Mono<Void>spring-doc.cadn.net.cn

执行给定的请求,并释放响应内容(如果有)。spring-doc.cadn.net.cn

Mono<HttpHeaders>spring-doc.cadn.net.cn

执行给定的请求,释放响应内容(如果有),并返回响应头。spring-doc.cadn.net.cn

Mono<T>spring-doc.cadn.net.cn

执行给定的请求,并将响应内容解码为声明的返回类型。spring-doc.cadn.net.cn

Flux<T>spring-doc.cadn.net.cn

执行给定的请求,并将响应内容解码为声明的元素类型的流。spring-doc.cadn.net.cn

Mono<ResponseEntity<Void>>spring-doc.cadn.net.cn

执行给定的请求,并释放响应内容(如果有),然后返回一个包含状态和头部信息的 ResponseEntityspring-doc.cadn.net.cn

Mono<ResponseEntity<T>>spring-doc.cadn.net.cn

执行给定的请求,将响应内容解码为声明的返回类型,并返回一个包含状态、头部和解码后主体的 ResponseEntityspring-doc.cadn.net.cn

Mono<ResponseEntity<Flux<T>>spring-doc.cadn.net.cn

执行给定的请求,将响应内容解码为声明的元素类型的流,并返回一个包含状态、头部和解码后的响应体流的ResponseEntityspring-doc.cadn.net.cn

默认情况下,同步返回值的超时时间设置为 ReactorHttpExchangeAdapter 时, 取决于底层HTTP客户端的配置方式。您也可以在适配器级别设置一个 blockTimeout 值,但我们建议依赖底层HTTP客户端的超时设置,因为它在更低的层级上运行,并提供更多的控制能力。spring-doc.cadn.net.cn

RestClientAdapter 提供了对返回类型为 InputStreamResponseEntity<InputStream> 的额外支持,这些类型可访问原始响应 体内容。spring-doc.cadn.net.cn

错误处理

为了自定义HTTP服务客户端代理的错误处理,您可以根据需要配置底层客户端。默认情况下,客户端会针对4xx和5xx的HTTP状态码抛出异常。要自定义此行为,可按照以下方式注册一个响应状态处理器,该处理器将应用于客户端执行的所有响应:spring-doc.cadn.net.cn

// 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();

更多详细信息及选项(如抑制错误状态码),请参阅每个客户端的参考文档,以及defaultStatusHandlerRestClient.BuilderWebClient.Builder中的Javadoc,和RestTemplatesetErrorHandlerspring-doc.cadn.net.cn

装饰适配器

HttpExchangeAdapterReactorHttpExchangeAdapter 是契约,用于解耦HTTP接口客户端基础设施与调用底层客户端的具体细节。存在针对 RestClientWebClientRestTemplate 的适配器实现。spring-doc.cadn.net.cn

有时,通过在HttpServiceProxyFactory.Builder中配置的装饰器来拦截客户端调用可能会很有用。例如,您可以应用内置装饰器来抑制404异常,并返回一个带有NOT_FOUNDnull内容体的ResponseEntity:spring-doc.cadn.net.cn

// 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-doc.cadn.net.cn

为了便于在大规模场景下与接口客户端协作,Spring框架提供了专门的配置支持。这使得应用程序能够专注于按组识别HTTP服务,并为每个组定制客户端,同时框架透明地创建客户端代理的注册表,并将每个代理声明为一个bean。spring-doc.cadn.net.cn

一个HTTP服务组简单来说就是一组接口,它们共享相同的客户端设置和 HttpServiceProxyFactory 实例来创建代理。通常,这意味着每个主机有一个组,但如果你需要为同一目标主机配置不同底层客户端的情况,也可以拥有多个组。spring-doc.cadn.net.cn

声明HTTP服务组的一种方式是通过在配置类中使用@RestController注解,如下所示:spring-doc.cadn.net.cn

@Configuration
@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) (1)
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) (2)
public class ClientConfig {
}
1 手动为“echo”组列出接口
2 检测基础包下"group greeting"的接口

也可以通过创建一个 HTTP 服务注册器并导入它来以编程方式声明组:spring-doc.cadn.net.cn

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 检测基础包下"group greeting"的接口
4 导入注册器
您可以将@ImportHttpService注解与程序注册器混合使用, 并且可以将导入分布在多个配置类中。所有导入 共同协作贡献相同的、共享的HttpServiceProxyRegistry实例。

一旦HTTP服务组被声明,添加一个HttpServiceGroupConfigurer bean来为每个组定制客户端。例如:spring-doc.cadn.net.cn

@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 可用,您可以方便地通过类型进行自动装配:spring-doc.cadn.net.cn

@RestController
public class EchoController {

	private final EchoService echoService;

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

	// ...
}

然而,如果存在同一类型的多个客户端代理,例如多个组中的相同接口,那么该类型就没有唯一的bean,您不能仅通过类型进行自动装配。对于这种情况,您可以直接使用持有所有代理的ApplicationContext,并通过组来获取所需的代理:spring-doc.cadn.net.cn

@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. 必须通过 headers(Consumer<HttpHeaders>)body(Object)RestClient 提供 HttpEntity 个请求头和请求体。
2. RequestEntity 方法、URI、请求头和请求体必须通过 RestClientmethod(HttpMethod)uri(URI)headers(Consumer<HttpHeaders>) 提供给 body(Object)