Spring 5 WebClient – Spring5的WebClient

最后修改: 2017年 7月 3日

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

1. Overview

1.概述

In this tutorial, we’re going to examine WebClient, which is a reactive web client introduced in Spring 5.

在本教程中,我们将研究WebClient,它是Spring 5中引入的一个反应式Web客户端。

We’re also going to look at the WebTestClient, a WebClient designed to be used in tests.

我们也要看看WebTestClient,一个WebClient,旨在用于测试。

2. What Is the WebClient?

2.什么是WebClient

Simply put, WebClient is an interface representing the main entry point for performing web requests.

简单地说,WebClient是一个代表执行网络请求的主要入口的接口。

It was created as part of the Spring Web Reactive module and will be replacing the classic RestTemplate in these scenarios. In addition, the new client is a reactive, non-blocking solution that works over the HTTP/1.1 protocol.

它是作为Spring Web Reactive模块的一部分创建的,并将在这些场景中取代经典的RestTemplate。此外,新的客户端是一个反应式的、非阻塞的解决方案,通过HTTP/1.1协议工作。

It’s important to note that even though it is, in fact, a non-blocking client and it belongs to the spring-webflux library, the solution offers support for both synchronous and asynchronous operations, making it suitable also for applications running on a Servlet Stack.

值得注意的是,尽管它实际上是一个非阻塞的客户端,并且属于spring-webflux库,但该解决方案提供了对同步和异步操作的支持,使其也适用于运行在Servlet Stack上的应用程序。

This can be achieved by blocking the operation to obtain the result. Of course, this practice is not suggested if we’re working on a Reactive Stack.

这可以通过阻断操作以获得结果来实现。当然,如果我们是在一个反应堆上工作,不建议采用这种做法。

Finally, the interface has a single implementation, the DefaultWebClient class, which we’ll be working with.

最后,该接口有一个单一的实现,即DefaultWebClient类,我们将与之合作。

3. Dependencies

3.依赖性

Since we are using a Spring Boot application, all we need is the spring-boot-starter-webflux dependency to obtain Spring Framework’s Reactive Web support.

由于我们使用的是Spring Boot应用程序,我们只需要spring-boot-starter-webflux依赖,以获得Spring框架的Reactive Web支持。

3.1. Building with Maven

3.1.用Maven构建

Let’s add the following dependencies to the pom.xml file:

让我们在pom.xml文件中添加以下依赖项。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

3.2. Building with Gradle

3.2.使用Gradle进行构建

With Gradle, we need to add the following entries to the build.gradle file:

使用Gradle,我们需要在build.gradle文件中添加以下条目。

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-webflux'
}

4. Working with the WebClient

4.使用WebClient工作

In order to work properly with the client, we need to know how to:

为了与客户正常工作,我们需要知道如何。

  • create an instance
  • make a request
  • handle the response

4.1. Creating a WebClient Instance

4.1.创建一个WebClient实例

There are three options to choose from. The first one is creating a WebClient object with default settings:

有三个选项可以选择。第一个是创建一个具有默认设置的WebClient对象。

WebClient client = WebClient.create();

The second option is to initiate a WebClient instance with a given base URI:

第二个选择是用一个给定的基本URI来启动一个WebClient实例。

WebClient client = WebClient.create("http://localhost:8080");

The third option (and the most advanced one) is building a client by using the DefaultWebClientBuilder class, which allows full customization:

第三种选择(也是最先进的一种)是通过使用DefaultWebClientBuilder类建立一个客户端,它允许完全定制。

WebClient client = WebClient.builder()
  .baseUrl("http://localhost:8080")
  .defaultCookie("cookieKey", "cookieValue")
  .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
  .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
  .build();

4.2. Creating a WebClient Instance with Timeouts

4.2.创建一个带有超时功能的WebClient实例

Oftentimes, the default HTTP timeouts of 30 seconds are too slow for our needs, to customize this behavior, we can create an HttpClient instance and configure our WebClient to use it.

很多时候,默认的30秒HTTP超时对于我们的需求来说太慢了,为了定制这种行为,我们可以创建一个HttpClient实例并配置我们的WebClient来使用它。

We can:

我们可以。

  • set the connection timeout via the ChannelOption.CONNECT_TIMEOUT_MILLIS option
  • set the read and write timeouts using a ReadTimeoutHandler and a WriteTimeoutHandler, respectively
  • configure a response timeout using the responseTimeout directive

As we said, all these have to be specified in the HttpClient instance we’ll configure:

正如我们所说,所有这些都必须在我们将要配置的HttpClient实例中指定。

HttpClient httpClient = HttpClient.create()
  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
  .responseTimeout(Duration.ofMillis(5000))
  .doOnConnected(conn -> 
    conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
      .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));

WebClient client = WebClient.builder()
  .clientConnector(new ReactorClientHttpConnector(httpClient))
  .build();

Note that while we can call timeout on our client request as well, this is a signal timeout, not an HTTP connection, a read/write, or a response timeout; it’s a timeout for the Mono/Flux publisher.

请注意,虽然我们也可以在客户端请求中调用timeout ,但这是一个信号超时,而不是HTTP连接、读/写或响应超时;它是Mono/Flux发布器的超时。

4.3. Preparing a Request – Define the Method

4.3.准备一个请求–定义方法

First, we need to specify an HTTP method of a request by invoking method(HttpMethod method):

首先,我们需要通过调用method(HttpMethod method)来指定一个请求的HTTP方法。

UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);

Or calling its shortcut methods such as get, post, and delete:

或调用其快捷方法,如getpostdelete

UriSpec<RequestBodySpec> uriSpec = client.post();

Note: although it might seem we reuse the request spec variables (WebClient.UriSpec, WebClient.RequestBodySpec, WebClient.RequestHeadersSpec, WebClient.ResponseSpec), this is just for simplicity to present different approaches. These directives shouldn’t be reused for different requests, they retrieve references, and therefore the latter operations would modify the definitions we made in previous steps.

注意:虽然看起来我们重复使用了请求规范变量(WebClient.UriSpec, WebClient.RequestBodySpec, WebClient.RequestHeadersSpec, WebClient.ResponseSpec),这只是为了简单地介绍不同方法。这些指令不应该在不同的请求中重复使用,它们检索的是引用,因此后面的操作会修改我们在前面步骤中的定义。

4.4. Preparing a Request – Define the URL

4.4.准备一个请求 – 定义URL

The next step is to provide a URL. Once again, we have different ways of doing this.

下一步是提供一个URL。再一次,我们有不同的方法来做这件事。

We can pass it to the uri API as a String:

我们可以把它作为一个字符串传递给uriAPI:

RequestBodySpec bodySpec = uriSpec.uri("/resource");

Using a UriBuilder Function:

使用一个UriBuilder函数

RequestBodySpec bodySpec = uriSpec.uri(
  uriBuilder -> uriBuilder.pathSegment("/resource").build());

Or as a java.net.URL instance:

或者作为一个java.net.URL实例。

RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));

Keep in mind that if we defined a default base URL for the WebClient, this last method would override this value.

请记住,如果我们为WebClient定义了一个默认的基本URL,最后这个方法将覆盖这个值。

4.5. Preparing a Request – Define the Body

4.5.准备一个请求–定义主体

Then we can set a request body, content type, length, cookies, or headers if we need to.

然后,如果需要的话,我们可以设置一个请求主体、内容类型、长度、cookies或头信息。

For example, if we want to set a request body, there are a few available ways. Probably the most common and straightforward option is using the bodyValue method:

例如,如果我们想设置一个请求体,有几种可用的方法。最常见和最直接的方法可能是使用bodyValue方法。

RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");

Or by presenting a Publisher (and the type of elements that will be published) to the body method:

或者通过向body方法提出Publisher(以及将被发布的元素的类型)。

RequestHeadersSpec<?> headersSpec = bodySpec.body(
  Mono.just(new Foo("name")), Foo.class);

Alternatively, we can make use of the BodyInserters utility class. For example, let’s see how we can fill in the request body using a simple object as we did with the bodyValue method:

另外,我们可以利用BodyInserters实用类。例如,让我们看看如何像使用bodyValue方法那样,使用一个简单的对象来填充请求体::。

RequestHeadersSpec<?> headersSpec = bodySpec.body(
  BodyInserters.fromValue("data"));

Similarly, we can use the BodyInserters#fromPublisher method if we are using a Reactor instance:

同样地,如果我们使用Reactor实例,我们可以使用BodyInserters#fromPublisher方法。

RequestHeadersSpec headersSpec = bodySpec.body(
  BodyInserters.fromPublisher(Mono.just("data")),
  String.class);

This class also offers other intuitive functions to cover more advanced scenarios. For instance, in case we have to send multipart requests:

该类还提供了其他直观的功能,以涵盖更高级的场景。例如,在我们必须发送多部分请求的情况下。

LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
  BodyInserters.fromMultipartData(map));

All these methods create a BodyInserter instance that we can then present as the body of the request.

所有这些方法都会创建一个BodyInserter实例,然后我们可以将其作为请求的body

The BodyInserter is an interface responsible for populating a ReactiveHttpOutputMessage body with a given output message and a context used during the insertion.

BodyInserter是一个接口,负责用给定的输出消息和插入期间使用的上下文来填充ReactiveHttpOutputMessage体。

A Publisher is a reactive component in charge of providing a potentially unbounded number of sequenced elements. It is an interface too, and the most popular implementations are Mono and Flux.

发布者是一个反应式组件,负责提供潜在的无限制数量的排序元素。它也是一个接口,最流行的实现是MonoFlux.

4.6. Preparing a Request – Define the Headers

4.6.准备一个请求–定义标头

After we set the body, we can set headers, cookies, and acceptable media types. Values will be added to those that have already been set when instantiating the client.

在我们设置了主体之后,我们可以设置头文件、cookies和可接受的媒体类型。值将被添加到那些在实例化客户端时已经设置的值中。

Also, there is additional support for the most commonly used headers like “If-None-Match”, “If-Modified-Since”, “Accept”, and “Accept-Charset”.

此外,还有对最常用的头信息的额外支持,如“If-None-Match”、”If-Modified-Since”、”Accept”,“Accept-Charset”。

Here’s an example of how these values can be used:

下面是一个如何使用这些数值的例子。

ResponseSpec responseSpec = headersSpec.header(
    HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
  .accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
  .acceptCharset(StandardCharsets.UTF_8)
  .ifNoneMatch("*")
  .ifModifiedSince(ZonedDateTime.now())
  .retrieve();

4.7. Getting a Response

4.7.获得回应

The final stage is sending the request and receiving a response. We can achieve this by using either the exchangeToMono/exchangeToFlux or the retrieve method.

最后一个阶段是发送请求并接收响应。我们可以通过使用exchangeToMono/exchangeToFluxretrieve方法来实现。

The exchangeToMono and exchangeToFlux methods allow access to the ClientResponse along with its status and headers:

exchangeToMonoexchangeToFlux方法允许访问ClientResponse及其状态和头文件。

Mono<String> response = headersSpec.exchangeToMono(response -> {
  if (response.statusCode().equals(HttpStatus.OK)) {
      return response.bodyToMono(String.class);
  } else if (response.statusCode().is4xxClientError()) {
      return Mono.just("Error response");
  } else {
      return response.createException()
        .flatMap(Mono::error);
  }
});

While the retrieve method is the shortest path to fetching a body directly:

虽然retrieve方法是直接获取一个主体的最短路径。

Mono<String> response = headersSpec.retrieve()
  .bodyToMono(String.class);

It’s important to pay attention to the ResponseSpec.bodyToMono method, which will throw a WebClientException if the status code is 4xx (client error) or 5xx (server error).

必须注意ResponseSpec.bodyToMono方法,如果状态代码是4xx(客户端错误)或5xx(服务器错误),它将抛出一个WebClientException

5. Working with the WebTestClient

5.使用WebTestClient工作

The WebTestClient is the main entry point for testing WebFlux server endpoints. It has a very similar API to the WebClient, and it delegates most of the work to an internal WebClient instance focusing mainly on providing a test context. The DefaultWebTestClient class is a single interface implementation.

WebTestClient是测试WebFlux服务器端点的主要入口。它的API与WebClient非常相似,它将大部分工作委托给内部的WebClient实例,主要侧重于提供一个测试环境。DefaultWebTestClient类是一个单一的接口实现。

The client for testing can be bound to a real server or work with specific controllers or functions.

用于测试的客户端可以绑定到一个真正的服务器上,或者与特定的控制器或功能一起工作。

5.1. Binding to a Server

5.1.绑定到一个服务器

To complete end-to-end integration tests with actual requests to a running server, we can use the bindToServer method:

为了用对运行中的服务器的实际请求完成端到端的集成测试,我们可以使用bindToServer方法。

WebTestClient testClient = WebTestClient
  .bindToServer()
  .baseUrl("http://localhost:8080")
  .build();

5.2. Binding to a Router

5.2.绑定到一个路由器

We can test a particular RouterFunction by passing it to the bindToRouterFunction method:

我们可以通过将一个特定的RouterFunction传递给bindToRouterFunction方法来测试它。

RouterFunction function = RouterFunctions.route(
  RequestPredicates.GET("/resource"),
  request -> ServerResponse.ok().build()
);

WebTestClient
  .bindToRouterFunction(function)
  .build().get().uri("/resource")
  .exchange()
  .expectStatus().isOk()
  .expectBody().isEmpty();

5.3. Binding to a Web Handler

5.3.绑定到一个网络处理程序

The same behavior can be achieved with the bindToWebHandler method, which takes a WebHandler instance:

同样的行为可以通过bindToWebHandler方法实现,该方法接收一个WebHandler实例。

WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();

5.4. Binding to an Application Context

5.4.绑定到一个应用上下文

A more interesting situation occurs when we’re using the bindToApplicationContext method. It takes an ApplicationContext and analyses the context for controller beans and @EnableWebFlux configurations.

当我们使用bindToApplicationContext方法时,会出现更有趣的情况。它需要一个ApplicationContext并分析控制器Bean和@EnableWebFlux配置的环境。

If we inject an instance of the ApplicationContext, a simple code snippet may look like this:

如果我们注入一个ApplicationContext的实例,一个简单的代码片断可能看起来像这样。

@Autowired
private ApplicationContext context;

WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
  .build();

5.5. Binding to a Controller

5.5.绑定到一个控制器

A shorter approach would be providing an array of controllers we want to test by the bindToController method. Assuming we’ve got a Controller class and we injected it into a needed class, we can write:

一个更简短的方法是通过bindToController方法提供一个我们想要测试的控制器数组。假设我们已经有了一个Controller类,并且把它注入到一个需要的类中,我们可以写。

@Autowired
private Controller controller;

WebTestClient testClient = WebTestClient.bindToController(controller).build();

5.6. Making a Request

5.6.提出请求

After building a WebTestClient object, all following operations in the chain are going to be similar to the WebClient until the exchange method (one way to get a response), which provides the WebTestClient.ResponseSpec interface to work with useful methods like the expectStatus, expectBody, and expectHeader:

在建立了一个WebTestClient对象后,链中的所有后续操作都将与WebClient类似,直到exchange方法(获得响应的一种方式),它提供了WebTestClient.ResponseSpec接口,以便与expectStatusexpectBodyexpectHeader等有用方法合作。

WebTestClient
  .bindToServer()
    .baseUrl("http://localhost:8080")
    .build()
    .post()
    .uri("/resource")
  .exchange()
    .expectStatus().isCreated()
    .expectHeader().valueEquals("Content-Type", "application/json")
    .expectBody().jsonPath("field").isEqualTo("value");

6. Conclusion

6.结语

In this article, we explored WebClient, a new enhanced Spring mechanism for making requests on the client-side.

在这篇文章中,我们探讨了WebClient,一种新的增强型Spring机制,用于在客户端发出请求。

We also looked at the benefits it provides by going through configuring the client, preparing the request, and processing the response.

我们还通过配置客户端、准备请求和处理响应来了解它提供的好处。

All of the code snippets mentioned in the article can be found in our GitHub repository.

文章中提到的所有代码片段都可以在我们的GitHub资源库中找到。