Spring WebClient 使用简介

现在,越来越多的项目都开始使用反应式编程以及异步处理请求了。在 Spring 5中,引入了反应式 WebClient实现作为 WebFlux 框架的一部分。今天,我们就来学习下如何使用 WebClient反应式地请求 REST API。

定义 REST API

首先,我们先定义一些 REST API(假设我们数据库里保存了一系列的事件,这些事件有id、属性、分类及标签等):

  • /events - 获取所有的事件
  • /events/[id] - 通过 id 获取事件
  • /events/[id]/atrributes/[attributeId] - 通过属性id获取某个事件的属性
  • /events?name=[name]&startDate=[startDate] - 根据给定条件获取事件
  • /events?tag[]=[tag1]&tag[]=[tag2] - 根据标签获取事件
  • /events?category=[category1]&category=[category2] - 根据分类获取事件

以上,我们定义了一些不同的 URI。等下,我们就来看下如何使用 WebClient 来构建和发送每种类型的 URI

需要注意的是,通过标签和分类查找事件都包含了数组查询参数,但是它们的语法不同。因为在 URI 中数组的表示没有严格定义,这主要取决于服务端的实现。在这里,我们两种方式都覆盖一下。

WebClient 设置

首先,我们需要创建一个WebClient实例。在这篇文章中,我们使用 mocked 的对象来验证是否请求了一个有效的 URI

我们先定义一个 client 以及相关的 mocked 对象。

this.exchangeFunction = mock(ExchangeFunction.class);
ClientResponse mockResponse = mock(ClientResponse.class);
when(this.exchangeFunction.exchange(this.argumentCaptor.capture())).thenReturn(Mono.just(mockResponse));
this.webClient = WebClient
		.builder()
		.baseUrl("https://example.com/api")
		.exchangeFunction(exchangeFunction)
		.build();

在上面的定义中,我们设置了一个参数 baseUrl,webClient 会将这个参数的值添加到它发送的所有请求之前。

最后,要验证特定的 URI 是否已经传递给底层的 ExchangeFunction 实例,我们需要使用以下方法:

private void verifyCalledUrl(String relativeUrl) {
	ClientRequest request = this.argumentCaptor.getValue();
	Assert.assertEquals(String.format("%s%s", BASE_URL, relativeUrl, request.url().toString());
	Mockito.verify(this.exchangeFunction).exchange(request);
	verifyNoMoreInteractions(this.exchangeFunction);
}

WebClientBuilder 类具有将 UriBuilder 实例作为参数提供的 uri() 方法。通常,API 调用通过以下方式进行:

this.webClient.get()
		.uri(uriBuilder -> uriBuilder
		// ... builder a URI
		.build())
	.retrieve());

后面,我们将广泛使用 UriBuilder 来构建 URI。需要注意的是,我们可以使用任何其它方式构建 URI,然后将生成的 URI 作为字符串传递进去。

URI 路径构成

URI 路径构成由一系列的由斜杠(/)分隔的路径段组成。首先,我们先看下最简单的没有任何可变段的/events简单案例。

this.webClient.get()
	.uri("/events")
	.retrieve();
verifyCalledUrl("/events");

在这个例子中,我们仅传递了一个String类型的数据作为参数。

接着,我们使用 /events/{id} 访问点并构建相应的 URI:

this.webClient.get()
	.uri(uriBuilder -> uriBuilder
		.path("/events/{id}")
		.build(2))
	.retrieve();
verifyCalledUrl("/events/2");

从上面的代码中,我们可以看到实际的 {id} 值 2 被传递给了 biild() 方法。

同样我们可以为 /events/{id}/attributes/{attributeId} 访问点创建一个包含多个路径段的 URI:

this.webClient.get()
	.uri(uriBuilder -> uriBuilder
		.path("/events/{id}/atrributes/{attributeId}")
		.build(3, 12))
	.retrieve();
verifyCalledUrl("/events/3/attributes/13");

在最终的 URI 长度没有超出限制的情况下,一个 URI 可以有很多路径段。我们只需要确保传递给 build() 方法的实际字段值的顺序需要正确。

URI 查询参数

通常情况下,一个查询参数是一个简单的键值对,比如 name=dengkaiting。我们来看下如何构建这样的 URI。

单值参数

我们从单值参数开始,采用 /events?name=[name]&startDate=[startDate]访问点。要设置查询参数,我们需要调用 UriBuilder 接口的 queryParam()方法:

this.webClient.get()
	.uri(uriBuilder -> uriBuilder
		.path("/events")
		.queryParam("name", "InitFailed")
		.queryParam("startDate", "13/02/2021")
		.build())
	.retrieve();
verifyCalledUrl("/events?name=InitFailed&startDate=13/02/2021")

我们添加了两个查询参数并给他们设置了实际值。此外,我们也可以使用占位符而不立即设置实际值:

this.webClient.get()
	.uri(uriBuilder -> uriBuilder
		.path("/events")
		.queryParam("name", "{name}")
		.queryParam("startDate", "{startDate}")
		.build("InitFailed", "13/02/2021"))
	.retrieve();
verifyCalledUrl("/events?name=InitFailed&startTime=13%2F02%2F2021")

当我们需要在调用链中进一步传递参数时,后面这种方式就比较方便了。上面两个代码片段里有一个重要的区别:
我们可以看到对于预期的 URI,它们两个的编码方式是不同的。在后面这个例子中,斜线(/)被转义了。一般来说,RFC3986不需要在查询中对斜杠进行编码。但是,某些服务端应用可能需要这种转换。我们在后面会介绍下如果更改此行为。

数组参数

有时,我们可能需要传递一个值数组。其实,在查询字符串中传递数组并没有严格的限制。对于数组的表示通常取决于底层框架。下面,我们介绍最广泛使用的格式。

我们以 /events?tag[]=[tag1]&tag[]=[tag2]访问点开始:

this.webClient.get()
	.uri(uriBuilder -> uriBuilder
		.path("/events")
		.queryParam("tag[]", "node", "service")
		.build())
	.retrieve();
verifyCalledUrl("/events?tag%5B%5D=node&tag%5B%5D=service")

最终的 URL 包含多个标记参数,后面紧跟编码后的方括号。queryParam()方法接受可变参数作为值,因此我们不需要多次调用该方法。

同样地,我们还可以省略方括号,只传递具有相同键但值不同的多个查询参数 - /events?category=[category1]&category=[category2]

this.webClient.get()
	.uri(uriBuilder -> uriBuilder
		.path("/events")
		.queryParams("category", "info", "warn")
		.build())
	.retrieve();
verifyCalledUrl("/events?category=info&category=warn")

还有一种更广泛使用的对数组进行编码的方法是传递逗号分隔的值。我们把前面的示例转为逗号分隔值:

this.webClient.get()
	.uri(uriBuilder -> uriBuilder
		.path("/events")
		.queryParam("category", String.join(",", "info", "warn"))
		.build())
	.retrieve();
verifyCalledUrl("/events?category=info,warn");

我们只是使用 String 类的 join() 方法创建了一个逗号分隔的字符串。当然,我们可以使用应用期望的任何其它分隔符。

编码模式

我们在上面提到了 URL 的编码。

如果默认行为不符合我们的要求,我们可以更改它。我们在创建WebClient实例的时候,可以提供一个 UriBuilderFactory 的实现来更改默认的编码模式。这里,我们使用 DefaultBuilderFactory类。要设置编码,需要调用 setEncodingMode() 方法。可以使用的模式有:

  • TEMPLATE_AND_VALUES:对 URI 模板进行预编码,扩展时对 URI 变量进行严格编码
  • VALUES_ONLY: 不对 URL 模板进行编码,而是将 URI 变量展开成模板后严格编码
  • URI_COMPONENTS:扩展 URI 变量后编码 URI 的组件值
  • NONE:不会应用任何编码

默认值是 TEMPLATE_AND_VALUES。我们把模式设置成 URI_COMPONENTS

DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
this.webClient = WebClient
	.builder()
	.uriBuilderFactory(factory)
	.baseUrl(BASE_URL)
	.exchangeFunction(exchangeFunction)
	.build();

最终,下面的断言可以成功:

this.webClient.get()
	.uri(uriBuilder -> uriBuilder)
		.path("/events")
		.queryParam("name", "InitFailed")
		.queryParam("startDate", "13/02/2021")
		.build()
	.retrieve();
verifyCalledUrl("/events?name=InitFailed&startDate=13/02/2021");

当然,我们也可以提供一个完全自定义的 URIBuilderFactory 实现来手动处理 URI 创建。

结论

在这篇文章中,我们了解了如何使用 WebClientDefaultUriBuilder 构建不同类型的 URI。在此过程中,我们介绍了各种类型和格式的查询参数。最后,我们更改了 URL 构建器的默认编码模式。


标题Spring WebClient 使用简介
作者末日没有进行曲
链接link
时间:2021-02-12
声明:本博客所有文章均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个使用Spring WebClient进行异步POST请求的代码范例,带有注释解释每个步骤的作用: ```java import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.time.Duration; public class WebClientExample { public static void main(String[] args) { // 创建一个WebClient实例 WebClient webClient = WebClient.builder() // 指定请求超时时间 .clientConnector(new ReactorClientHttpConnector()) .defaultHeader("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE) .defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE) .baseUrl("http://localhost:8080") .build(); // 构造请求参数 MultiValueMap<String, String> formData = new LinkedMultiValueMap<>(); formData.add("param1", "value1"); formData.add("param2", "value2"); // 发送POST请求 Mono<ClientResponse> responseMono = webClient.post() // 指定请求路径 .uri("/api/path") // 设置请求体 .body(BodyInserters.fromFormData(formData)) // 发送请求并返回响应结果 .exchange(); // 处理响应结果 responseMono.subscribe(response -> { // 打印响应状态码 System.out.println(response.statusCode()); // 打印响应头 response.headers().asHttpHeaders().forEach((name, values) -> { System.out.println(name + ": " + values); }); // 打印响应体 response.bodyToMono(String.class).subscribe(System.out::println); }); } } ``` 在这个例子中,我们创建了一个WebClient实例,并指定了请求超时时间、默认请求头、请求的基础URL等信息。然后,我们构造了一个包含请求参数的MultiValueMap对象,并将其作为请求体发送POST请求。 WebClient的post()方法返回一个RequestHeadersSpec对象,该对象可以用于设置请求头、请求体等信息。在这个例子中,我们使用uri()方法指定了请求路径,并使用body()方法设置了请求体。 最后,我们调用exchange()方法发送请求并返回响应结果。exchange()方法返回一个Mono<ClientResponse>对象,我们可以使用subscribe()方法对其进行订阅,然后在回调函数中处理响应结果。在这个例子中,我们打印了响应状态码、响应头和响应体。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值