REST 客户端
Spring 框架提供了以下几种方式用于调用 REST 端点:
-
RestClient— 具有流畅 API 的同步客户端 -
WebClient— 非阻塞、响应式客户端,提供流畅的 API -
RestTemplate— 同步客户端,采用模板方法 API,现已弃用,推荐使用RestClient -
HTTP 服务客户端 — 由生成的代理支持的注解接口
RestClient
RestClient 是一个同步的 HTTP 客户端,提供流畅的 API 来执行请求。
它作为对 HTTP 库的抽象,负责将 HTTP 请求和响应内容与更高层次的 Java 对象相互转换。
创建一个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。
这一步是可选的,如果你通过构建器(builder)配置了 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 请求。
对于可以包含请求体的 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() 是一个空操作(no-op),并返回一个 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)
| 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 请求,该 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)
| 1 | 创建一个 Pet 领域对象 |
| 2 | 设置一个 POST 请求以及要连接的 URL |
| 3 | 将 Content-Type 请求头设置为 application/json |
| 4 | 使用 pet 作为请求体 |
| 5 | 将响应转换为一个不含响应体的 ResponseEntity。 |
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 | 将响应转换为一个不含响应体的 ResponseEntity。 |
错误处理
默认情况下,当接收到状态码为 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() 时不会应用状态处理器(status handlers),因为 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();
文件上传
要发送 multipart 数据,您需要提供一个 MultiValueMap<String, Object>,其值可以是用于部分(part)内容的 Object、用于文件部分的 Resource,或带有头部信息的 HttpEntity 用于部分(part)内容。
例如:
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 类型,则根据文件扩展名自动确定。
如有必要,您可以使用 MediaType 包装器显式提供 HttpEntity。
一旦 MultiValueMap 准备就绪,你就可以将其用作 POST 请求的请求体,通过 RestClient.post().body(parts)(或 RestTemplate.postForObject)来发送。
如果 MultiValueMap 包含至少一个非 String 的值,则 Content-Type 将由 FormHttpMessageConverter 设置为 multipart/form-data。
如果 MultiValueMap 具有 String 个值,则 Content-Type 默认为 application/x-www-form-urlencoded。
如有必要,也可以显式设置 Content-Type。
客户端请求工厂
为了执行 HTTP 请求,RestClient 使用了一个客户端 HTTP 库。
这些库通过 ClientRequestFactory 接口进行适配。
目前提供了多种实现:
-
JdkClientHttpRequestFactory表示 Java 的HttpClient -
用于 Apache HTTP Components
HttpComponentsClientHttpRequestFactory的HttpClient -
JettyClientHttpRequestFactory代表 Jetty 的HttpClient -
ReactorNettyClientRequestFactory用于 Reactor Netty 的HttpClient -
SimpleClientHttpRequestFactory作为一个简单的默认实现
如果在构建 RestClient 时未指定请求工厂,它将使用 classpath 中可用的 Apache 或 Jetty HttpClient。
否则,如果已加载 java.net.http 模块,则会使用 Java 自带的 HttpClient。
最后,它将退而使用简单的默认实现。
请注意,SimpleClientHttpRequestFactory 在访问表示错误(例如 401)的响应状态时可能会抛出异常。
如果这是一个问题,请使用任意一种替代的请求工厂。 |
WebClient
WebClient 是一个用于执行 HTTP 请求的非阻塞、响应式客户端。它在 5.0 版本中引入,作为 RestTemplate 的替代方案,支持同步、异步和流式处理场景。
WebClient 支持以下功能:
-
非阻塞 I/O
-
响应式流背压
-
使用更少的硬件资源实现高并发
-
利用 Lambda 表达式的函数式、流畅 API
-
同步与异步交互
-
向服务器上传流或从服务器下载流
有关更多详情,请参见WebClient。
RestTemplate
RestTemplate 以经典的 Spring 模板类形式,为 HTTP 客户端库提供了高级别的 API。
它公开了以下几组重载方法:
自 Spring Framework 7.0 起,RestTemplate 已被弃用,推荐使用 RestClient,并在未来版本中移除,
请参阅“迁移到 RestClient”指南。
对于异步和流式处理场景,请考虑使用响应式的WebClient。 |
| 方法组 | 描述 |
|---|---|
|
通过 GET 请求获取一个表示。 |
|
使用 GET 方法获取一个 |
|
通过使用 HEAD 方法检索资源的所有标头。 |
|
通过使用 POST 创建一个新资源,并从响应中返回 |
|
通过使用 POST 创建一个新资源,并返回响应中的表示形式。 |
|
通过使用 POST 创建一个新资源,并返回响应中的表示形式。 |
|
通过使用 PUT 创建或更新资源。 |
|
通过使用 PATCH 方法更新资源,并返回响应中的资源表示形式。
请注意,JDK 的 |
|
使用 DELETE 方法删除指定 URI 处的资源。 |
|
通过使用 ALLOW 方法检索资源所允许的 HTTP 方法。 |
|
前述方法的一个更通用(且更少预设)的版本,在需要时提供了额外的灵活性。
它接受一个 这些方法允许使用 |
|
通过回调接口对请求准备和响应提取进行完全控制,从而以最通用的方式执行请求。 |
初始化
RestTemplate 使用与 RestClient 相同的 HTTP 库抽象。
默认情况下,它使用 SimpleClientHttpRequestFactory,但可以通过构造函数进行更改。
参见 客户端请求工厂。
RestTemplate 可以进行可观测性(observability)插桩,以生成指标(metrics)和追踪(traces)。
参见RestTemplate 可观测性支持一节。 |
身体
传入和从 RestTemplate 方法返回的对象,会借助 HttpMessageConverter 转换为 HTTP 消息或从 HTTP 消息转换而来,参见HTTP 消息转换。
迁移到RestClient
应用程序可以逐步采用 RestClient,首先关注 API 的使用,然后再进行基础设施的设置。
您可以考虑以下步骤:
-
从现有的
RestClient实例创建一个或多个RestTemplate,例如:RestClient restClient = RestClient.create(restTemplate)。 通过首先专注于发起请求,逐步在您的应用程序中逐个组件地替换RestTemplate的使用。 有关 API 对应关系,请参见下表。 -
一旦所有客户端请求都经过
RestClient个实例,您现在就可以使用RestClient.Builder来复制现有的RestTemplate实例创建。由于RestTemplate和RestClient共享相同的基础设施,您可以在设置中重用自定义的ClientHttpRequestFactory或ClientHttpRequestInterceptor。请参阅 theRestClientbuilder API。
如果类路径中没有其他可用的库,RestClient 将选择由现代 JDK JdkClientHttpRequestFactory 驱动的 HttpClient,而 RestTemplate 则会选择使用 SimpleClientHttpRequestFactory 的 HttpURLConnection。这可能导致在 HTTP 层面出现细微的运行时行为差异。
下表展示了 RestClient 方法对应的 RestTemplate 等效用法。
RestTemplate 方法 |
RestClient 的等效用法 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RestClient 和 RestTemplate 实例在抛出异常方面具有相同的行为
(RestClientException 类型位于异常层次结构的顶端)。
当 RestTemplate 对于“4xx”响应状态始终抛出 HttpClientErrorException 时,
RestClient 则通过自定义 “状态处理器” 提供了更大的灵活性。
HTTP 服务客户端
你可以将一个 HTTP 服务定义为一个包含 @HttpExchange 方法的 Java 接口,并使用 HttpServiceProxyFactory 通过 RestClient、WebClient 或 RestTemplate 创建客户端代理,以通过 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 方法支持灵活的方法签名,可接受以下输入:
| 方法参数 | 描述 |
|---|---|
|
动态设置请求的 URL,覆盖注解中的 |
|
提供一个 |
|
动态设置请求的 HTTP 方法,覆盖注解中的 |
|
添加一个请求头或多个请求头。参数可以是单个值、值的 |
|
添加一个变量,用于在请求 URL 中展开占位符。该参数可以是一个包含多个变量的 |
|
提供一个 |
|
以待序列化的对象形式提供请求体,或者以响应式流(Reactive Streams) |
|
添加一个或多个请求参数。参数可以是包含多个参数的 当 |
|
添加一个请求部分,该部分可以是字符串(表单字段)、 |
|
从 |
|
添加一个或多个 Cookie。参数可以是一个包含多个 Cookie 的 |
方法参数不能为 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 的客户端(例如 RestClient 和 RestTemplate)
支持同步返回值:
| 方法返回值 | 描述 |
|---|---|
|
执行给定的请求。 |
|
执行给定的请求并返回响应头。 |
|
执行给定的请求,并将响应内容解码为声明的返回类型。 |
|
执行给定的请求,并返回一个包含状态和头部信息的 |
|
执行给定的请求,将响应内容解码为声明的返回类型,并返回一个包含状态、头部信息和解码后响应体的 |
诸如 ReactorHttpExchangeAdapter 等适配了 WebClient 的客户端,不仅支持上述所有内容,还支持响应式变体。下表展示了 Reactor 类型,但你也可以使用通过 ReactiveAdapterRegistry 支持的其他响应式类型:
| 方法返回值 | 描述 |
|---|---|
|
执行给定的请求,并释放响应内容(如果有的话)。 |
|
执行给定的请求,释放响应内容(如果有的话),并返回响应头。 |
|
执行给定的请求,并将响应内容解码为声明的返回类型。 |
|
执行给定的请求,并将响应内容解码为所声明元素类型的流。 |
|
执行给定的请求,释放响应内容(如果有的话),并返回一个包含状态和头部信息的 |
|
执行给定的请求,将响应内容解码为声明的返回类型,并返回一个包含状态、头部信息和解码后响应体的 |
|
执行给定的请求,将响应内容解码为声明元素类型的流,并返回一个包含状态、头部信息和已解码响应体流的 |
默认情况下,使用 ReactorHttpExchangeAdapter 处理同步返回值时的超时时间取决于底层 HTTP 客户端的配置方式。你也可以在适配器级别设置一个 blockTimeout 值,但我们建议依赖底层 HTTP 客户端的超时设置,因为它在更低的层级运行,并提供更精细的控制。
RestClientAdapter 为返回类型为 InputStream 或 ResponseEntity<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();
有关更多详细信息和选项(例如抑制错误状态码),请参阅每个客户端的参考文档,以及 defaultStatusHandler 或 RestClient.Builder 中 WebClient.Builder 的 Javadoc,还有 setErrorHandler 的 RestTemplate 方法。
装饰适配器
HttpExchangeAdapter 和 ReactorHttpExchangeAdapter 是用于将 HTTP 接口客户端基础设施与底层客户端调用细节解耦的契约。目前已提供了针对 RestClient、WebClient 和 RestTemplate 的适配器实现。
有时,通过在 HttpServiceProxyFactory.Builder 中配置的装饰器来拦截客户端调用可能会很有用。例如,您可以应用内置的装饰器来抑制 404 异常,并返回一个状态为 ResponseEntity 且主体为 NOT_FOUND 的 null:
// 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 服务组只是一组共享相同客户端配置和同一个 HttpServiceProxyFactory 实例来创建代理的接口。通常,这意味着每个主机对应一个服务组,但如果底层客户端需要不同的配置,你也可以为同一个目标主机创建多个服务组。
声明 HTTP 服务组的一种方式是通过在 @ImportHttpServices 类中使用 @Configuration 注解,如下所示:
@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 服务注册器(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 {
}
| 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 客户端代理 |
RequestEntity、RestClient、method(HttpMethod) 和 uri(URI) 向 headers(Consumer<HttpHeaders>) 提供 body(Object) 的方法、URI、请求头和请求体。