REST 客户端
Spring框架提供了以下几种方式来调用REST端点:
- RestClient — 具有简洁API的同步客户端
- WebClient — 具有简洁API的非阻塞、响应式客户端
- RestTemplate — 具有模板方法API的同步客户端,现已弃用,推荐使用
RestClient - HTTP Service Clients — 由生成的代理支持的带注解的接口
RestClient
RestClient 是一个同步的 HTTP 客户端,它提供了一个流畅的 API 来执行请求。它作为 HTTP 库的抽象层,负责将 HTTP 请求和响应内容转换为更高层次的 Java 对象,以及反过来将 Java 对象转换成 HTTP 响应和请求内容。
创建一个 RestClient
RestClient 提供了静态的 create 简写方法。它还提供了一个 builder(),该方法允许设置更多选项:
一旦创建,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的形式提供,在这两种情况下URL都不会被编码。有关处理和编码URI的更多详细信息,请参阅URI链接。
请求头和请求体
如有必要,可以通过使用 header(String, String)、headers(Consumer<HttpHeaders>),以及便捷方法 accept(MediaType…)、acceptCharset(Charset…) 等来修改 HTTP 请求头。对于可以包含请求体的 HTTP 请求(如 POST、PUT 和 PATCH),还有其他可用的方法: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 执行终端操作(terminal operation)才能产生任何副作用。如果你的用例中不需要使用响应内容,你可以使用 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
设置一个GET请求
指定要连接的URL
获取响应
将响应转换为字符串
打印结果
val result = restClient.get() 1
uri("https://example.com") 2
retrieve() 3
.body<String>() 4
println(result) 5
设置一个GET请求
指定要连接的URL
获取响应
将响应转换为字符串
打印结果
通过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
为指定的URL设置一个GET请求
将响应转换为
ResponseEntity打印结果
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
为指定的URL设置一个GET请求
将响应转换为
ResponseEntity打印结果
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
使用 URI 变量
将
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
使用 URI 变量
将
Accept头部设置为application/json- \ [#3] 将 JSON 响应转换为
Pet类型对象
在下一个示例中,RestClient被用来执行一个包含JSON数据的POST请求,该JSON数据再次通过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
创建一个
Pet域对象设置 POST 请求以及连接的 URL
将
Content-Type头设置为application/json使用
pet作为请求体将响应转换为一个没有内容的响应实体。
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
创建一个
Pet域对象设置 POST 请求以及连接的 URL
将
Content-Type头设置为application/json使用
pet作为请求体将响应转换为一个没有内容的响应实体。
错误处理
默认情况下,当RestClient检索到状态码为4xx或5xx的响应时,会抛出一个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
Throws new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); 3
})
.body(String.class);
为返回404状态码的URL创建一个GET请求
为所有4xx状态码设置状态处理程序
抛出一个自定义异常
val result = restClient.get() 1
uri("https://example.com/this-url-does-not-exist") 1
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError) { _, response -> 2
Throws MyCustomRuntimeException(response.statusCode(), response.getHeaders()) } 3
.body<String>()
为返回404状态码的URL创建一个GET请求
为所有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(responsegetStatusCode(), response.getHeaders()); 2
}
else {
Pet pet = convertResponse(response); 3
return pet;
}
});
exchange提供请求和响应当响应的的状态码为 4xx 时抛出异常
将响应转换成 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.statusCode(), response.getHeaders()) 2
} else {
val pet: Pet = convertResponse(response) 3
pet
}
}
exchange提供请求和响应当响应的状态码为 4xx 时抛出异常
将响应转换成 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
要发送多部分数据,你需要提供一个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 的HttpClientHttpComponentsClientHttpRequestFactory用于与 Apache HTTP Components 的HttpClient一起使用JettyClientHttpRequestFactory用于 Jetty 的HttpClientReactorNettyClientRequestFactory用于 Reactor Netty 的HttpClientSimpleClientHttpRequestFactory作为简单的默认选项
如果在构建RestClient时没有指定请求工厂(request factory),那么如果类路径(classpath)中存在Apache或Jetty的HttpClient,它将使用这些客户端。否则,如果加载了java.net.http模块,它将使用Java自带的HttpClient。如果以上两种方式都不可行,它将回退到默认的简单实现。
请注意,SimpleClientHttpRequestFactory在访问表示错误的响应状态(例如401)时可能会抛出异常。如果遇到这种情况,请使用其他请求工厂之一。
WebClient
WebClient是一个非阻塞的、响应式的客户端,用于执行HTTP请求。它从Java 5.0版本开始引入,作为RestTemplate的替代方案,支持同步、异步和流式处理场景。
WebClient 支持以下功能:
- 非阻塞I/O(Non-blocking I/O)
- 反应式流背压(Reactive Streams back pressure)
- 使用更少的硬件资源实现高并发(High concurrency with fewer hardware resources)
- 采用函数式风格、流畅的API,充分利用Java 8的Lambda表达式(Functional-style, fluent API that takes advantage of Java 8 lambdas)
- 同步与异步交互(Synchronous and asynchronous interactions)
- 从服务器进行数据流上传或下载(Streaming up to or streaming down from a server)
有关更多详细信息,请参阅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 可以被用于可观测性(observability)的监控,以便生成指标(metrics)和跟踪数据(traces)。请参阅 RestTemplate 可观测性支持 部分。
正文
传递给RestTemplate方法并从中返回的对象,通过HttpMessageConverter的帮助被转换为HTTP消息,反之亦然。详情请参见HTTP消息转换。
迁移至 RestClient
应用程序可以逐步采用RestClient,首先专注于API的使用,然后才是基础设施的设置。您可以考虑以下步骤:
-
从现有的
RestTemplate实例中创建一个或多个RestClient,例如:RestClient restClient = RestClient.create(restTemplate)。逐步在您的应用程序中替换对RestTemplate的使用,先从发送请求的部分开始。请参见下表以获取相应的 API 对应关系。 -
一旦所有客户端请求都通过
RestClient实例进行,您现在就可以使用RestClient.Builder来复制现有的RestTemplate实例创建方式了。由于RestTemplate和RestClient共享相同的基础设施,因此您可以在设置中重用自定义的ClientHttpRequestFactory或ClientHttpRequestInterceptor。请参阅 RestClient 构造器 API。
如果在类路径(classpath)中没有其他可用的库,RestClient将会选择由现代JDK的HttpClient支持的JdkClientHttpRequestFactory,而RestTemplate则会选择使用HttpURLConnection的SimpleClientHttpRequestFactory。这可以解释在运行时HTTP层面上出现的细微行为差异。
下表显示了RestTemplate方法的RestClient等效实现。
表2. RestClient中RestTemplate方法的对应实现
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) |
RestClient和RestTemplate在抛出异常时表现出相同的行为(异常层次结构的顶端是RestClientException类型)。当RestTemplate对于“4xx”响应状态一致地抛出HttpClientErrorException时,RestClient则允许通过自定义的“状态处理器”(#rest-http-service-client-exceptions)提供更大的灵活性。
HTTP服务客户端
你可以将HTTP服务定义为具有@HttpExchange方法的Java接口,并使用HttpServiceProxyFactory从该接口创建一个客户端代理,以便通过RestClient、WebClient或RestTemplate进行远程HTTP访问。在服务器端,一个@Controller类可以实现相同的接口,使用带有@HttpExchange注解的控制器方法来处理请求(参见../web/webmvc/mvc-controller/ann-requestmapping.md#mvc-ann-httpexchange-annotation)。
首先,创建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作为请求属性添加。仅由RestClient和WebClient支持。 |
@RequestBody | 提供请求体,可以是需要序列化的Object,也可以是Reactive Streams的Publisher(如Mono、Flux,或是通过配置的ReactiveAdapterRegistry支持的任何其他异步类型)。 |
@RequestParam | 添加一个或多个请求参数。参数可以是一个Map<String, ?>或MultiValueMap<String, ?>类型的值集合,或者是一个单独的值。非字符串类型的值支持类型转换。当“content-type”被设置为“application/x-www-form-urlencoded”时,请求参数会编码在请求体中;否则,它们将作为URL查询参数添加。 |
@RequestPart | 添加一个请求部分,可以是字符串(表单字段)、Resource(文件部分)、Object(需要编码的实体,例如JSON格式)、HttpEntity(包含内容和头的部分)、Spring的Part,或是上述类型的Reactive Streams Publisher。 |
MultipartFile | 添加一个来自MultipartFile的请求部分,通常在Spring MVC控制器中使用,用于表示上传的文件。 |
@CookieValue | 添加一个或多个cookie。参数可以是一个Map<String, ?>或MultiValueMap<String, ?>类型的值集合,或者是一个单独的值。非字符串类型的值支持类型转换。 |
方法参数不能为null,除非将required属性(在参数注解中可用)设置为false,或者根据[MethodParameter#isOptional](https://docs.spring.io/spring-framework/docs/7.0.3/javadoc-api/org/springframework/core/MethodParameter.html#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的客户端,如RestClient和RestTemplate,支持同步返回值:
| 方法返回值 | 描述 |
|---|---|
void | 执行给定的请求。 |
HttpHeaders | 执行给定的请求并返回响应头。 |
<T> | 执行给定的请求,并将响应内容解码为声明的返回类型。 |
ResponseEntityVOID> | 执行给定的请求,并返回一个包含状态码和响应头的 ResponseEntity。 |
ResponseEntity<T> | 执行给定的请求,将响应内容解码为声明的返回类型,并返回一个包含状态码、响应头及解码后内容的 ResponseEntity。 |
适用于ReactorHttpExchangeAdapter的客户端,如WebClient,不仅支持上述所有功能,还支持反应式(reactive)变体。下表列出了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客户端的超时设置,因为底层HTTP客户端在更低的级别上运行,并且提供了更多的控制能力。
RestClientAdapter 提供了对返回值为 InputStream 或 ResponseEntity<InputStream> 类型的额外支持,这种类型可以访问原始响应体内容。
错误处理
要自定义HTTP Service客户端代理的错误处理,可以根据需要配置底层的客户端。默认情况下,客户端会对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.Builder或WebClient.Builder中的defaultStatusHandler的Javadoc,还有RestTemplate的setErrorHandler。
装饰适配器
HttpExchangeAdapter 和 ReactorHttpExchangeAdapter 是一些契约(contracts),它们将 HTTP 接口客户端基础设施与调用底层客户端的细节解耦开来。对于 RestClient、WebClient 和 RestTemplate,都存在相应的适配器实现(adapter implementations)。
有时,通过HttpServiceProxyFactory.Builder中可配置的装饰器来拦截客户端调用可能会很有用。例如,你可以应用内置的装饰器来抑制404异常,并返回一个带有NOT_FOUND状态码和null主体的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框架提供了专门的配置支持。它允许应用程序专注于按组识别HTTP服务,并为每个组定制客户端,而框架则会透明地创建一个客户端代理注册表,并将每个代理声明为一个bean。
HTTP服务组(HTTP Service group)简单来说就是一组共享相同客户端配置以及HttpServiceProxyFactory实例的接口,用于创建代理。通常情况下,每个主机对应一个服务组,但对于同一个目标主机,如果底层客户端需要不同的配置,也可以设置多个服务组。
一种声明HTTP服务组的方法是通过在@Configuration类中使用@ImportHttpServices注解,如下所示:
@Configuration
@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) 1
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) 2
public class ClientConfig {
}
手动列出“echo”组的接口
检测基础包下“greeting”组的接口
也可以通过创建一个HTTP服务注册器(HTTP Service registrar),然后导入它来以编程方式声明这些组:
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 {
}
创建
AbstractHttpServiceRegistrar的扩展类手动列出“echo”组的接口
- \ [#3] 检测基础包下的“greeting”组的接口
导入注册器
你可以将@ImportHttpService注解与程序化的注册器(programmatic registrars)结合使用,并且可以将导入操作分散到多个配置类中。所有这些导入操作共同贡献出同一个共享的HttpServiceProxyRegistry实例。
一旦声明了HTTP服务组,就添加一个HttpServiceGroupConfigurerbean来为每个组定制客户端。例如:
@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了,你就不能仅通过类型进行自动连接(autowire)了。对于这种情况,你可以直接使用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
}
// ...
}
访问“echo1”组的
EchoService客户端代理访问“echo2”组的
EchoService客户端代理
1. HttpEntity 的头部和正文需要通过 headers(Consumer<HttpHeaders>) 和 body(Object) 提供给 RestClient。
2. 需要通过 method(HttpMethod), uri(URI), headers(Consumer<HttpHeaders>) 和 body(Object) 将 RequestEntity 方法、URI、头部信息(headers)以及请求体(body)提供给 RestClient。