51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring 6 中的声明式 HTTP 接口

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: 添加请求头信息,参数可以是 MapMultiValueMap
  • @PathVariable:替换请求 URL 中的占位符参数。
  • @RequestBody:提供的请求体可以是要序列化的对象,也可以是响应式流 publisher(如 Mono 或 Flux)。
  • @RequestParam:添加请求参数,参数可以是 MapMultiValueMap
  • @CookieValue:添加 cookie,参数可以是 MapMultiValueMap

注意,只有 Content Type 为 application/x-www-form-urlencoded 的请求才会在请求体中对请求参数进行编码。否则,请求参数将作为 URL 查询参数添加。

2.3、返回值 {#23返回值}

在我们的示例接口中,exchange 方法返回的是阻塞式的普通值。声明式 HTTP 接口 exchange 方法既支持阻塞式的返回值,也支持响应式返回值。

此外,我们可以选择只返回特定的响应信息,如状态码或响应头。如果我们对服务响应完全不感兴趣,也可以返回 void

总之,HTTP 接口 exchange 方法支持以下返回值:

  • voidMono<Void>:执行请求并丢弃响应内容。
  • HttpHeadersMono<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

赞(2)
未经允许不得转载:工具盒子 » Spring 6 中的声明式 HTTP 接口