1. 概述
随着响应式编程和异步请求处理在各大框架中的普及,Spring 5 也顺应趋势推出了基于 WebFlux 的 WebClient 实现。
本教程将带你掌握如何使用 WebClient 响应式地调用 REST API 接口,重点在于如何构造包含路径参数和查询参数的请求。
2. REST API 接口定义
为了便于讲解,我们先定义一组示例接口:
/products
– 获取所有产品/products/{id}
– 根据 ID 获取产品/products/{id}/attributes/{attributeId}
– 获取产品属性/products/?name={name}&deliveryDate={deliveryDate}&color={color}
– 按条件搜索产品/products/?tag[]={tag1}&tag[]={tag2}
– 按标签获取产品(数组形式)/products/?category={category1}&category={category2}
– 按分类获取产品(多值参数)
这些接口涵盖了常见的 URI 参数形式。接下来我们会用 WebClient 构造对应的请求。
⚠️ 注意:数组参数在 URL 中没有统一格式,具体格式取决于服务端实现,我们会在示例中覆盖主流写法。
3. WebClient 初始化
在使用 WebClient 前,需要先创建实例。为了演示请求构建过程,我们使用 Mockito 模拟底层调用逻辑:
exchangeFunction = mock(ExchangeFunction.class);
ClientResponse mockResponse = mock(ClientResponse.class);
when(mockResponse.bodyToMono(String.class))
.thenReturn(Mono.just("test"));
when(exchangeFunction.exchange(argumentCaptor.capture()))
.thenReturn(Mono.just(mockResponse));
webClient = WebClient
.builder()
.baseUrl("https://example.com/api")
.exchangeFunction(exchangeFunction)
.build();
为验证请求是否正确构造,我们定义一个辅助方法:
private void verifyCalledUrl(String relativeUrl) {
ClientRequest request = argumentCaptor.getValue();
assertEquals(String.format("%s%s", BASE_URL, relativeUrl), request.url().toString());
verify(this.exchangeFunction).exchange(request);
verifyNoMoreInteractions(this.exchangeFunction);
}
WebClient 的请求一般如下构造:
webClient.get()
.uri(uriBuilder -> uriBuilder
//... building a URI
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
我们会大量使用 UriBuilder
来构造 URI,也可以直接传入字符串 URI。
4. 路径参数(URI Path)
路径参数是 URL 中由 /
分隔的片段。我们从最简单的 /products
开始:
webClient.get()
.uri("/products")
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products");
对于 /products/{id}
这样的带变量路径,使用如下方式:
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/{id}")
.build(2))
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/2");
多个路径参数同理,如 /products/{id}/attributes/{attributeId}
:
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/{id}/attributes/{attributeId}")
.build(2, 13))
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/2/attributes/13");
✅ 注意顺序:传入 build()
的参数顺序必须与路径变量顺序一致。
5. 查询参数(Query Parameters)
查询参数通常以 key=value
形式出现在 URL 的 ?
后面。
5.1 单值参数
以 /products/?name={name}&deliveryDate={deliveryDate}&color={color}
为例:
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/")
.queryParam("name", "AndroidPhone")
.queryParam("color", "black")
.queryParam("deliveryDate", "13/04/2019")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");
也可以使用占位符:
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/")
.queryParam("name", "{title}")
.queryParam("color", "{authorId}")
.queryParam("deliveryDate", "{date}")
.build("AndroidPhone", "black", "13/04/2019"))
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13%2F04%2F2019");
⚠️ 注意编码差异:/
被转义成了 %2F
,这取决于编码模式设置。
5.2 数组参数
数组参数格式没有统一标准,常见的有以下几种:
5.2.1 使用 []
表示数组
适用于 /products/?tag[]={tag1}&tag[]={tag2}
:
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/")
.queryParam("tag[]", "Snapdragon", "NFC")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?tag%5B%5D=Snapdragon&tag%5B%5D=NFC");
5.2.2 多个同名参数
适用于 /products/?category={category1}&category={category2}
:
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/")
.queryParam("category", "Phones", "Tablets")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?category=Phones&category=Tablets");
5.2.3 逗号分隔字符串
适用于逗号分隔格式:
webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/")
.queryParam("category", String.join(",", "Phones", "Tablets"))
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?category=Phones,Tablets");
✅ 灵活选择:根据服务端要求选择合适的数组参数格式。
6. URL 编码模式(Encoding Mode)
WebClient 提供了多种编码模式来控制 URL 的编码行为:
模式 | 说明 |
---|---|
TEMPLATE_AND_VALUES |
默认值,预编码模板,变量替换时也严格编码 |
VALUES_ONLY |
不编码模板,仅编码变量 |
URI_COMPONENT |
替换变量后对组件进行编码 |
NONE |
完全不编码 |
我们可以通过如下方式修改编码模式:
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
webClient = WebClient
.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.exchangeFunction(exchangeFunction)
.build();
这样可以避免 /
被编码成 %2F
,从而适应某些服务端的要求。
✅ 自定义实现:你也可以实现自己的 UriBuilderFactory
来完全控制 URL 构造和编码逻辑。
7. 总结
本文介绍了如何使用 WebClient 构造包含路径参数和查询参数的请求,涵盖:
- 基本路径参数构造
- 单值查询参数
- 数组参数的多种写法(
[]
、多键、逗号分隔) - URL 编码模式的配置
所有示例代码已上传至 GitHub,地址如下:
如果你在使用 WebClient 时遇到参数构造问题,不妨参考本文方法。踩坑不怕,关键是找到最合适的写法。