1、概览 {#1概览}
在 Spring 6 和 Spring Boot 3 中,我们可以使用 Java 接口来定义声明式的远程 HTTP 服务。这种方法受到 Feign 等流行 HTTP 客户端库的启发,与在 Spring Data 中定义 Repository 的方法类似。
在本教程中,我们将首先了解如何定义 HTTP 接口,以及可用的 exchange
方法注解和支持的方法参数和返回值。接着,学习如何创建一个实际的 HTTP 接口实例,即执行所声明 HTTP exchange
的代理客户端。
最后,我们将介绍如何对声明式 HTTP 接口及其代理客户端进行异常处理和测试。
2、HTTP 接口 {#2http-接口}
声明式 HTTP 接口包括用于 HTTP exchange 的注解方法。我们可以通过使用带注解的 Java 接口来简单地表达远程 API 的细节,然后让 Spring 生成实现该接口并执行 exchange 的代理。这有助于减少样板代码的编写。
2.1、Exchange 方法 {#21exchange-方法}
@HttpExchange
是我们可以应用于 HTTP 接口及其 exchange 方法的根注解。如果我们将其应用于接口层,那么它就会应用于所有 exchange 方法。这对于指定所有接口方法的共同属性(如 content type 或 URL 前缀)非常有用。
所有 HTTP 方法都有对应的注解:
@GetExchange
用于 HTTP GET 请求。@PostExchange
用于 HTTP POST 请求。@PutExchange
用于 HTTP PUT 请求。@PatchExchange
用于 HTTP PATCH 请求。@DelectExchange
用于 HTTP DELETE 请求。
让我们使用不同的 HTTP 方法注解,来为远程 API 定义一个声明式的 HTTP 接口:
interface BooksService {
@GetExchange("/books")
List<Book> getBooks();
@GetExchange("/books/{id}")
Book getBook(@PathVariable long id);
@PostExchange("/books")
Book saveBook(@RequestBody Book book);
@DeleteExchange("/books/{id}")
ResponseEntity<Void> deleteBook(@PathVariable long id);
}
注意,所有 HTTP 方法注解都是用 @HttpExchange
元注解的。因此,@GetExchange("/books")
等同于 @HttpExchange(url = "/books",method = "GET")
。
2.2、方法参数 {#22方法参数}
在上述示例接口中,我们在方法参数中使用了 @PathVariable
和 @RequestBody
注解。此外,我们还可以为 exchange 方法使用以下参数、注解:
URI
: 动态设置请求的 URL,覆盖注解属性。HttpMethod
:动态设置请求的 HTTP 方法,覆盖注解属性。@RequestHeader
: 添加请求头信息,参数可以是Map
或MultiValueMap
。@PathVariable
:替换请求 URL 中的占位符参数。@RequestBody
:提供的请求体可以是要序列化的对象,也可以是响应式流 publisher(如 Mono 或 Flux)。@RequestParam
:添加请求参数,参数可以是Map
或MultiValueMap
。@CookieValue
:添加 cookie,参数可以是Map
或MultiValueMap
。
注意,只有 Content Type 为 application/x-www-form-urlencoded
的请求才会在请求体中对请求参数进行编码。否则,请求参数将作为 URL 查询参数添加。
2.3、返回值 {#23返回值}
在我们的示例接口中,exchange 方法返回的是阻塞式的普通值。声明式 HTTP 接口 exchange 方法既支持阻塞式的返回值,也支持响应式返回值。
此外,我们可以选择只返回特定的响应信息,如状态码或响应头。如果我们对服务响应完全不感兴趣,也可以返回 void
。
总之,HTTP 接口 exchange 方法支持以下返回值:
void
、Mono<Void>
:执行请求并丢弃响应内容。HttpHeaders
、Mono<HttpHeaders>
: 执行请求,丢弃响应体,返回响应头。<T>
、Mono<T>
:执行请求,并将响应体解码为所声明的类型。<T>
、Flux<T>
:执行请求,并将响应体解码为所声明类型的数据流。ResponseEntity<Void>
、Mono<ResponseEntity<Void>>
:执行请求,丢弃响应体,并返回一个包含状态和响应头的ResponseEntity
。ResponseEntity<T>
、Mono<ResponseEntity<T>>
:执行请求,并返回一个包含状态、响应头和解码后的响应体ResponseEntity
。Mono<ResponseEntity<Flux<T>>
:执行请求,并返回一个包含状态、响应头和解码后的响应体ResponseEntity
。
我们还可以使用 ReactiveAdapterRegistry
中注册的任何其他异步或响应式类型。
3、客户端代理实现 {#3客户端代理实现}
既然我们已经定义了 HTTP 服务接口,就需要创建一个代理来实现该接口并执行 exchange。
3.1、Proxy Factory {#31proxy-factory}
Spring 为我们提供了一个 HttpServiceProxyFactory
,我们可以用它为 HTTP 接口生成一个客户端代理:
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(webClient))
.build();
booksService = httpServiceProxyFactory.createClient(BooksService.class);
要使用提供的工厂创建代理,除了 HTTP 接口之外,我们还需要一个响应式 Web 客户端的实例:
WebClient webClient = WebClient.builder()
.baseUrl(serviceUrl)
.build();
现在,我们可以将客户端代理实例注册为 Spring Bean 或组件,并用它请求 REST 服务。
3.2、异常处理 {#32异常处理}
默认情况下,WebClient
会对任何客户端或服务器错误 HTTP 状态代码抛出 WebClientResponseException
。我们可以通过注册一个默认的 response status handler 来自定义异常处理,该 handler 适用于通过客户端执行的所有响应:
BooksClient booksClient = new BooksClient(WebClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, resp ->
Mono.just(new MyServiceException("Custom exception")))
.baseUrl(serviceUrl)
.build());
如此一来,如果我们请求的 book 不存在,我们就会收到一个自定义异常:
BooksService booksService = booksClient.getBooksService();
assertThrows(MyServiceException.class, () -> booksService.getBook(9));
4、测试 {#4测试}
让我们看看如何测试我们的示例中声明式 HTTP 接口,以及执行交互的客户端代理。
4.1、使用 Mockito
{#41使用-mockito}
由于我们的目标是测试使用声明式 HTTP 接口创建的客户端代理,因此需要使用 Mockito 的 deep stubbing 功能来模拟底层 WebClient
的 fluent API:
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private WebClient webClient;
现在,我们可以使用 Mockito
的 BDD 方法链式调用 WebClient
方法,并提供模拟响应:
given(webClient.method(HttpMethod.GET)
.uri(anyString(), anyMap())
.retrieve()
.bodyToMono(new ParameterizedTypeReference<List<Book>>(){}))
.willReturn(Mono.just(List.of(
new Book(1,"Book_1", "Author_1", 1998),
new Book(2, "Book_2", "Author_2", 1999)
)));
模拟响应就绪后,我们就可以使用 HTTP 接口定义的方法调用我们的服务了:
BooksService booksService = booksClient.getBooksService();
Book book = booksService.getBook(1);
assertEquals("Book_1", book.title());
4.2、使用 MockServer
{#42使用-mockserver}
如果我们不想模拟 WebClient
,可以使用 MockServer
这样的库生成并返回固定的 HTTP 响应:
new MockServerClient(SERVER_ADDRESS, serverPort)
.when(
request()
.withPath(PATH + "/1")
.withMethod(HttpMethod.GET.name()),
exactly(1)
)
.respond(
response()
.withStatusCode(HttpStatus.SC_OK)
.withContentType(MediaType.APPLICATION_JSON)
.withBody("{\"id\":1,\"title\":\"Book_1\",\"author\":\"Author_1\",\"year\":1998}")
);
现在已经准备好了模拟的响应和正在运行的模拟服务器(mock server,),可以调用我们的服务了。
BooksClient booksClient = new BooksClient(WebClient.builder()
.baseUrl(serviceUrl)
.build());
BooksService booksService = booksClient.getBooksService();
Book book = booksService.getBook(1);
assertEquals("Book_1", book.title());
此外,还可以验证我们的测试代码是否调用了正确的模拟服务。
mockServer.verify(
HttpRequest.request()
.withMethod(HttpMethod.GET.name())
.withPath(PATH + "/1"),
VerificationTimes.exactly(1)
);
5、总结 {#5总结}
在本文中,我们介绍了 Spring 6 中的声明式 HTTP 服务接口。我们了解了如何使用不同的 HTTP 方法注解来定义 exchange 接口方法,以及支持的方法参数和返回值。
此外,我们还了解了如何通过自定义 response status handler 来执行异常处理。最后,我们了解了如何使用 Mockito 和 MockServer 测试声明式接口及其客户端代理实现。
参考:https://www.baeldung.com/spring-6-http-interface