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
:动态覆盖注解中的 URLHttpMethod
:动态覆盖 HTTP 方法@RequestHeader
:添加请求头(支持Map
或MultiValueMap
)@PathVariable
:替换 URL 路径占位符@RequestBody
:设置请求体(支持对象或Mono
/Flux
)@RequestParam
:添加查询参数(支持Map
或MultiValueMap
)@CookieValue
:添加 Cookie(支持Map
或MultiValueMap
)
⚠️ 注意:只有 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 仓库