欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 会展 > springboot3 声明式 HTTP 接口

springboot3 声明式 HTTP 接口

2025/4/26 9:43:41 来源:https://blog.csdn.net/qq_40298351/article/details/147505715  浏览:    关键词:springboot3 声明式 HTTP 接口

1 介绍

        在 Spring 6 和 Spring Boot 3 中,我们可以使用 Java 接口来定义声明式的远程 HTTP 服务。这种方法受到 Feign 等流行 HTTP 客户端库的启发,与在 Spring Data 中定义 Repository 的方法类似。

        声明式 HTTP 接口包括用于 HTTP exchange 的注解方法。我们可以通过使用带注解的 Java 接口来简单地表达远程 API 的细节,然后让 Spring 生成实现该接口并执行 exchange 的代理。这有助于减少样板代码的编写。

1.1 Exchange 方法

   @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")

1.2 方法参数

        在上述示例接口中,我们在方法参数中使用了 @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 查询参数添加。

1.3 返回值

        在我们的示例接口中,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 中注册的任何其他异步或响应式类型。

2 客户端代理实现

        既然我们已经定义了 HTTP 服务接口,就需要创建一个代理来实现该接口并执行 exchange。

2.1 Proxy 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 服务。

2.2 异常处理

        默认情况下,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));

3 测试

        让我们看看如何测试我们的示例中声明式 HTTP 接口,以及执行交互的客户端代理。

3.1 使用 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());

3.2 使用 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)
);

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词