1. 概述

Spring Framework 6 和 Spring Boot 3 引入了一项新特性:使用 Java 接口声明式定义 HTTP 服务。这个设计灵感来源于 Feign 等流行 HTTP 客户端库,与 Spring Data 中定义 Repository 的方式类似。

本文将带你:

  • ✅ 定义声明式 HTTP 接口
  • ✅ 掌握支持的注解、参数和返回值类型
  • ✅ 创建实际可用的代理客户端
  • ✅ 处理异常和测试方案

2. HTTP 接口

声明式 HTTP 接口通过注解方法定义 HTTP 交互。只需用注解标注 Java 接口,Spring 就会自动生成代理实现,极大减少模板代码。

2.1. 交换方法

@HttpExchange核心注解,可用于接口级别或方法级别。在接口上使用时,会对所有方法生效(比如统一设置 Content-Type 或 URL 前缀)。

针对具体 HTTP 方法的专用注解:

  • @GetExchange → GET 请求
  • @PostExchange → POST 请求
  • @PutExchange → PUT 请求
  • @PatchExchange → PATCH 请求
  • @DeleteExchange → DELETE 请求

定义一个简单的 REST 服务接口示例:

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);
}

💡 所有方法专用注解都是 @HttpExchange 的元注解。例如:@GetExchange("/books") 等价于 @HttpExchange(url = "/books", method = "GET")

2.2. 方法参数

支持以下参数类型:

  • URI:动态覆盖注解中的 URL
  • HttpMethod:动态覆盖 HTTP 方法
  • @RequestHeader:添加请求头(支持 MapMultiValueMap
  • @PathVariable:替换 URL 路径占位符
  • @RequestBody:设置请求体(支持对象或 Mono/Flux
  • @RequestParam:添加查询参数(支持 MapMultiValueMap
  • @CookieValue:添加 Cookie(支持 MapMultiValueMap

⚠️ 注意:只有 Content-Type 为 application/x-www-form-urlencoded 时,请求参数才会编码到请求体中,否则作为 URL 查询参数。

2.3. 返回值

支持阻塞式和响应式返回类型,包括:

  • void/Mono<Void>:执行请求并释放响应
  • HttpHeaders/Mono<HttpHeaders>:仅返回响应头
  • <T>/Mono<T>:返回解码后的响应体
  • <T>/Flux<T>:返回解码后的响应流
  • ResponseEntity<Void>/Mono<ResponseEntity<Void>>:返回状态+头信息
  • ResponseEntity<T>/Mono<ResponseEntity<T>>:返回状态+头+解码体
  • Mono<ResponseEntity<Flux<T>>>:返回状态+头+解码流

也支持 ReactiveAdapterRegistry 中注册的其他异步/响应式类型。

3. 客户端代理

定义好接口后,需要创建代理实现来执行 HTTP 请求。

3.1. 代理工厂

使用 HttpServiceProxyFactory 创建代理:

HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
  .builder(WebClientAdapter.forClient(webClient))
  .build();
booksService = httpServiceProxyFactory.createClient(BooksService.class);

需要先配置 WebClient 实例:

WebClient webClient = WebClient.builder()
  .baseUrl(serviceUrl)
  .build();

将代理注册为 Spring Bean 后即可注入使用。

3.2. 异常处理

默认情况下,WebClient 对 4xx/5xx 状态码抛出 WebClientResponseException。可通过自定义状态处理器覆盖:

BooksClient booksClient = new BooksClient(WebClient.builder()
  .defaultStatusHandler(HttpStatusCode::isError, resp ->
    Mono.just(new MyServiceException("自定义异常")))
  .baseUrl(serviceUrl)
  .build());

测试不存在的资源时:

BooksService booksService = booksClient.getBooksService();
assertThrows(MyServiceException.class, () -> booksService.getBook(9));

4. 测试方案

4.1. 使用 Mockito

需要深度模拟 WebClient 的链式调用

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private WebClient webClient;

使用 BDD 风格设置模拟响应:

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)
  )));

调用接口方法验证:

BooksService booksService = booksClient.getBooksService();
Book book = booksService.getBook(1);
assertEquals("Book_1", book.title());

4.2. 使用 MockServer

避免直接模拟 WebClient,改用 MockServer 返回固定响应:

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}")
  );

调用服务并验证:

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. 总结

我们系统学习了 Spring 6 的声明式 HTTP 接口特性:

  • 通过注解定义接口,Spring 自动生成代理实现
  • 支持所有标准 HTTP 方法和灵活的参数/返回值类型
  • 结合 WebClient 实现底层调用
  • 提供自定义异常处理机制
  • 支持 Mockito 和 MockServer 两种测试方案

完整示例代码见 GitHub 仓库


原始标题:HTTP Interface in Spring | Baeldung