Spring WebClient Requests with Parameters – 带有参数的Spring WebClient请求

最后修改: 2019年 4月 30日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

A lot of frameworks and projects are introducing reactive programming and asynchronous request handling. As such, Spring 5 introduced a reactive WebClient implementation as part of the WebFlux framework.

很多框架和项目正在引入反应式编程和异步请求处理。因此,Spring 5引入了一个反应式WebClient的实现,作为WebFlux框架的一部分。

In this tutorial, we’ll learn how to reactively consume REST API endpoints with WebClient.

在本教程中,我们将学习如何用WebClient主动地消费REST API端点。

2. REST API Endpoints

2.REST API端点

To start, let’s define a sample REST API with the following GET endpoints:

首先,让我们定义一个样本REST API,其中包含以下GET端点

  • /products – get all products
  • /products/{id} – get product by ID
  • /products/{id}/attributes/{attributeId} – get product attribute by id
  • /products/?name={name}&deliveryDate={deliveryDate}&color={color} – find products
  • /products/?tag[]={tag1}&tag[]={tag2} – get products by tags
  • /products/?category={category1}&category={category2} – get products by categories

Here we defined a few different URIs. In just a moment, we’ll figure out how to build and send each type of URI with WebClient.

这里我们定义了几个不同的URI。稍后,我们将弄清楚如何用WebClient构建和发送每种类型的URI。

Please note that the URIs for gettings products by tags and categories contain arrays as query parameters; however, the syntax differs because there’s no strict definition of how arrays should be represented in URIs. This primarily depends on the server-side implementation. Accordingly, we’ll cover both cases.

请注意,按标签和类别获取产品的URI包含数组作为查询参数;但是,语法不同,因为没有严格定义数组应该如何在URI中表示。这主要取决于服务器端的实现。因此,我们将涵盖这两种情况。

3. WebClient Setup

3.WebClientSetup

First, we’ll need to create an instance of WebClient. For this article, we’ll be using a mocked object to verify that a valid URI is requested.

首先,我们需要创建一个WebClient的实例。在本文中,我们将使用一个模拟对象来验证所请求的有效URI。

Let’s define the client and related mock objects:

让我们来定义客户端和相关的模拟对象。

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

We’ll also pass a base URL that will be prepended to all requests made by the client.

我们还将传递一个基本的URL,这个URL将被预置到客户端的所有请求中。

Finally, to verify that a particular URI has been passed to the underlying ExchangeFunction instance, we’ll use the following helper method:

最后,为了验证一个特定的URI已经被传递给底层的ExchangeFunction实例,我们将使用以下帮助方法。

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

The WebClientBuilder class has the uri() method that provides the UriBuilder instance as an argument. Generally, we make an API call in the following manner:

WebClientBuilder类有uri()方法,提供UriBuilder实例作为参数。一般来说,我们以如下方式进行API调用。

webClient.get()
  .uri(uriBuilder -> uriBuilder
    //... building a URI
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

We’ll use UriBuilder extensively in this guide to construct URIs. It’s worth noting that we can build a URI using other methods, and then just pass the generated URI as a String.

我们将在本指南中广泛使用UriBuilder来构建URI。值得注意的是,我们可以使用其他方法来构建URI,然后将生成的URI作为一个字符串传递。

4. URI Path Component

4.URI路径组件

A path component consists of a sequence of path segments separated by a slash ( / ). First, we’ll start with a simple case where a URI doesn’t have any variable segments, /products:

一个路径组件由一串由斜线 ( / )分隔的路径段组成。首先,我们从一个简单的案例开始,URI没有任何变量段,/products

webClient.get()
  .uri("/products")
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products");

For this case, we can just pass a String as an argument.

对于这种情况,我们可以只传递一个String作为参数。

Next, we’ll take the /products/{id} endpoint and build the corresponding URI:

接下来,我们将采取/products/{id}端点并建立相应的URI。

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}")
    .build(2))
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/2");

From the code above, we can see that the actual segment values are passed to the build() method.

从上面的代码中,我们可以看到,实际的段值被传递给build()方法。

In a similar way, we can create a URI with multiple path segments for the /products/{id}/attributes/{attributeId} endpoint:

以类似的方式,我们可以为/products/{id}/attributes/{attributeId}端点创建一个具有多个路径段的URI。

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}/attributes/{attributeId}")
    .build(2, 13))
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/2/attributes/13");

A URI can have as many path segments as required, though the final URI length must not exceed limitations. Finally, we need to remember to keep the right order of actual segment values passed to the build() method.

一个URI可以有任意多的路径段,尽管最终的URI长度不能超过限制。最后,我们需要记住保持传递给build()方法的实际段值的正确顺序。

5. URI Query Parameters

URI查询参数

Usually, a query parameter is a simple key-value pair like title=Baeldung. Let’s see how to build such URIs.

通常,查询参数是一个简单的键值对,如title=Baeldung。让我们看看如何建立这样的URI。

5.1. Single Value Parameters

5.1.单一值参数

We’ll start with single value parameters and take the /products/?name={name}&deliveryDate={deliveryDate}&color={color} endpoint. To set a query parameter, we’ll call the queryParam() method of the UriBuilder interface:

我们将从单值参数开始,采取/products/?name={name}&deliveryDate={deliveryDate}&color={color}端点。为了设置一个查询参数,我们将调用UriBuilder接口的queryParam()方法。

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "AndroidPhone")
    .queryParam("color", "black")
    .queryParam("deliveryDate", "13/04/2019")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");

Here we added three query parameters and assigned actual values immediately. Conversely, it’s also possible to leave placeholders instead of exact values:

这里我们添加了三个查询参数,并立即分配了实际值。反之,也可以留下占位符,而不是准确的值。

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

This might be especially helpful when passing a builder object further in a chain.

当在一个链中进一步传递一个构建器对象时,这可能特别有帮助。

Note that there’s one important difference between the two code snippets above. With attention to the expected URIs, we can see that they’re encoded differently. Particularly, the slash character ( / ) was escaped in the last example.

请注意,上面的两个代码片段之间有一个重要的区别。注意预期的URI,我们可以看到它们的编码是不同的。特别是,斜线字符( / )在最后一个例子中被转义了。

Generally speaking, RFC3986 doesn’t require the encoding of slashes in the query; however, some server-side applications might require such conversion. Therefore, we’ll see how to change this behavior later in this guide.

一般来说,RFC3986不需要对查询中的斜线进行编码;但是,一些服务器端的应用程序可能需要进行这样的转换。因此,我们将在本指南的后面看到如何改变这种行为。

5.2. Array Parameters

5.2.阵列参数

We might need to pass an array of values, and there aren’t strict rules for passing arrays in a query string. Therefore, an array representation in a query string differs from project to project, and usually depends on underlying frameworks. We’ll cover the most widely used formats in this article.

我们可能需要传递一个数组的值,而在查询字符串中传递数组并没有严格的规则。因此,查询字符串中的数组表示法因项目而异,而且通常取决于底层框架。我们将在本文中介绍最广泛使用的格式。

Let’s start with the /products/?tag[]={tag1}&tag[]={tag2} endpoint:

让我们从/products/?tag[]={tag1}&tag[]={tag2}端点开始。

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("tag[]", "Snapdragon", "NFC")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?tag%5B%5D=Snapdragon&tag%5B%5D=NFC");

As we can see, the final URI contains multiple tag parameters, followed by encoded square brackets. The queryParam() method accepts variable arguments as values, so there’s no need to call the method several times.

我们可以看到,最终的URI包含多个标签参数,后面是编码的方括号。queryParam()方法接受变量参数作为值,所以没有必要多次调用该方法。

Alternatively, we can omit square brackets and just pass multiple query parameters with the same key, but different values, /products/?category={category1}&category={category2}:

另外,我们可以去掉方括号,只传递多个具有相同键的查询参数,但不同的值,/products/?category={category1}&category={category2}

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("category", "Phones", "Tablets")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?category=Phones&category=Tablets");

Finally, there’s one more extensively-used method to encode an array, which is to pass comma-separated values. Let’s transform our previous example into comma-separated values:

最后,还有一种更广泛使用的方法来对数组进行编码,那就是传递逗号分隔的值。让我们把前面的例子转换成逗号分隔的值。

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("category", String.join(",", "Phones", "Tablets"))
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?category=Phones,Tablets");

We’re just using the join() method of the String class to create a comma-separated string. We can also use any other delimiter that’s expected by the application.

我们只是使用String类的join()方法来创建一个逗号分隔的字符串。我们也可以使用应用程序所期望的任何其他分隔符。

6. Encoding Mode

6.编码模式

Remember how we previously mentioned URL encoding?

还记得我们之前提到的URL编码吗?

If the default behavior doesn’t fit our requirements, we can change it. We need to provide a UriBuilderFactory implementation while building a WebClient instance. In this case, we’ll use the DefaultUriBuilderFactory class. To set encoding, we’ll call the setEncodingMode() method. The following modes are available:

如果默认行为不符合我们的要求,我们可以改变它。我们需要在构建WebClient实例时提供一个UriBuilderFactory实现。在这种情况下,我们将使用DefaultUriBuilderFactory类。为了设置编码,我们将调用setEncodingMode()方法。有以下几种模式可供选择。

  • TEMPLATE_AND_VALUES: Pre-encode the URI template and strictly encode URI variables when expanded
  • VALUES_ONLY: Do not encode the URI template, but strictly encode URI variables after expanding them into the template
  • URI_COMPONENTS: Encode URI component value after expending URI variables
  • NONE: No encoding will be applied

The default value is TEMPLATE_AND_VALUES. Let’s set the mode to URI_COMPONENTS:

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

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

As a result, the following assertion will succeed:

因此,下面的断言将会成功。

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "AndroidPhone")
    .queryParam("color", "black")
    .queryParam("deliveryDate", "13/04/2019")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");

And, of course, we can provide a completely custom UriBuilderFactory implementation to handle URI creation manually.

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

7. Conclusion

7.结语

In this article, we learned how to build different types of URIs using WebClient and DefaultUriBuilder.

在这篇文章中,我们学习了如何使用WebClientDefaultUriBuilder构建不同类型的URI。

Along the way, we covered various types and formats of query parameters. Finally, we wrapped up by changing the default encoding mode of the URL builder.

一路走来,我们涵盖了查询参数的各种类型和格式。最后,我们通过改变URL生成器的默认编码模式进行了总结。

As always, all of the code snippets from the article are available over on GitHub repository.

一如既往,文章中的所有代码片段都可以在GitHub仓库上找到。