WebClient 超时设置

1、概览

WebClient 是一个响应式的 HTTP 客户端,它基于 Reactor 项目提供了函数式 API。

本文将带你了解 WebClient 的超时设置,学习如何正确地设置不同的超时,既包括整个应用程序的全局超时,也包括特定请求的超时。

2、WebClient 和 HTTP 客户端

WebClient 还需要一个 HTTP 客户端库才能正常工作。Spring 为其中一些提供了 内置支持,默认使用的是 Reactor Netty

大多数配置,包括超时,都可以通过这些客户端完成。

3、通过 HTTP 客户端配置超时

如前所述,在应用中设置不同 WebClient 超时的最简单方法是通过底层 HTTP 客户端进行全局设置。这也是最有效的方法。

由于 Netty 是 Spring WebFlux 的默认客户端库,本文将以 Reactor Netty HttpClient 类 作为示例。

3.1、响应超时

响应超时是指发送请求后等待接收响应的时间。可以使用 responseTimeout() 方法为客户端配置它:

HttpClient client = HttpClient.create()
  .responseTimeout(Duration.ofSeconds(1)); 

在本例中,配置的超时时间为 1 秒。Netty 默认不设置响应超时。

然后,就可以使用 HttpClient 来构建 Spring WebClient

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

之后,WebClient 会继承底层 HttpClient 提供的所有配置,适用于发送的所有请求。

3.2、连接超时

连接超时是客户端和服务器之间建立连接的必须时间段。可以使用不同的 Channel Option Key 和 option() 方法来执行配置:

HttpClient client = HttpClient.create()
 .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

// 创建 WebClient

提供的值以毫秒为单位,将超时设置为 10 秒。Netty 默认值 为30 秒。

此外,还可以配置 keep-alive 选项,它可以在连接空闲时发送 TCP 保活探测报文。

HttpClient client = HttpClient.create()
  .option(ChannelOption.SO_KEEPALIVE, true)
  .option(EpollChannelOption.TCP_KEEPIDLE, 300)
  .option(EpollChannelOption.TCP_KEEPINTVL, 60)
  .option(EpollChannelOption.TCP_KEEPCNT, 8);

// 创建 WebClient

如上,启用了 keep-alive,在空闲 5 分钟后以 60 秒为间隔进行探测。还将连接中断前的最大探测次数设置为 8 次。

当连接在给定时间内未建立或被放弃时,会抛出 ConnectTimeoutException 异常。

3.3、读写超时

读取超时发生在一定时间内没有读取数据,而写入超时发生在写入操作无法在特定时间内完成。 HttpClient 允许配置额外的 Handler 来配置这些超时:

HttpClient client = HttpClient.create()
  .doOnConnected(conn -> conn
    .addHandler(new ReadTimeoutHandler(10, TimeUnit.SECONDS))
    .addHandler(new WriteTimeoutHandler(10)));

// 创建 WebClient

通过 doOnConnected() 方法配置连接回调,并在其中添加了 ReadTimeOutHandlerWriteTimeOutHandler 实例处理读写超时,超时时间都设置为 10 秒。

这些 Handler 的构造函数接受两种不同的参数。可以指定 TimeUnit 以及对应的值,或者只指定一个值,默认的单位是秒。

底层的 Netty 库提供了 ReadTimeoutExceptionWriteTimeoutException 异常类来表示对应的超时异常。

3.4、SSL/TLS 超时

握手超时是系统在停止操作之前尝试建立 SSL 连接的时间。可以通过 secure() 方法设置 SSL 配置:

HttpClient.create()
  .secure(spec -> spec.sslContext(SslContextBuilder.forClient())
    .defaultConfiguration(SslProvider.DefaultConfigurationType.TCP)
    .handshakeTimeout(Duration.ofSeconds(30))
    .closeNotifyFlushTimeout(Duration.ofSeconds(10))
    .closeNotifyReadTimeout(Duration.ofSeconds(10)));

// 创建 WebClient

如上,将握手超时设置为 30 秒(默认值:10 秒),将 close_notify flush(默认值:3 秒)和 read(默认值:0 秒)超时设置为 10 秒。所有方法都由 SslProvider.Builder 接口提供。

当由于超时而导致握手失败时,将抛出 SslHandshakeTimeoutException

3.5、代理超时

HttpClient 还支持代理功能。如果与对等端的连接在指定时间内未完成,连接将失败。可以通过 proxy() 设置这个超时时间:

HttpClient.create()
  .proxy(spec -> spec.type(ProxyProvider.Proxy.HTTP)
    .host("proxy")
    .port(8080)
    .connectTimeoutMillis(30000));

// 创建 WebClient

使用 connectTimeoutMillis() 将超时时间设置为 30 秒,默认值为 10 秒。

在连接失败时,Netty 会抛出 ProxyConnectException

4、请求级超时

在上一节中,使用 HttpClient 全局配置了不同的超时。不过,也可以独立于全局设置,设置特定请求响应的超时。

4.1、使用 HttpClientRequest 设置响应超时

在请求级别配置响应超时:

webClient.get()
  .uri("https://baeldung.com/path")
  .httpRequest(httpRequest -> {
    HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
    reactorRequest.responseTimeout(Duration.ofSeconds(2));
  });

如上,使用 WebClienthttpRequest() 方法来访问底层 Netty 库的原生 HttpClientRequest。接着,将超时值设置为 2 秒。

该设置会覆盖 HttpClient 层面的任何响应超时设置。也可以将此值设置为 null,以删除任何先前配置的值。

4.2、配置 “响应式流” 超时

Reactor Netty 使用 Reactor Core 作为 Reactive Stream 实现。可以使用 MonoFlux Publisher 提供的 timeout() 方法来配置另一个超时:

webClient.get()
  .uri("https://baeldung.com/path")
  .retrieve()
  .bodyToFlux(JsonNode.class)
  .timeout(Duration.ofSeconds(5));

在这种情况下,如果在 5 秒内没有项目到达,就会出现 TimeoutException 异常。

最好使用 Reactor Netty 中提供的更具体的超时配置选项,因为它们能为特定目的和用例提供更多控制。

timeout() 方法适用于整个操作,包括与远程建立连接到接收响应。它不会覆盖任何与 HttpClient 相关的设置。

5、Exception 处理

每种类型的超时都会产生一个专门的异常,因此可以使用 Ractive Stream 和 onError 来处理它们:

webClient.get()
  .uri("https://baeldung.com/path")
  .retrieve()
  .bodyToFlux(JsonNode.class)
  .timeout(Duration.ofSeconds(5))
  .onErrorMap(ReadTimeoutException.class, ex -> new HttpTimeoutException("ReadTimeout"))
  .onErrorReturn(SslHandshakeTimeoutException.class, new TextNode("SslHandshakeTimeout"))
  .doOnError(WriteTimeoutException.class, ex -> log.error("WriteTimeout"))
  ...

可以重用之前描述的任何异常,并使用 Reactor 编写自己的处理方法。

此外,还可以根据 HTTP 状态码添加一些逻辑:

webClient.get()
  .uri("https://baeldung.com/path")
  .onStatus(HttpStatus::is4xxClientError, resp -> {
    log.error("ClientError {}", resp.statusCode());
    return Mono.error(new RuntimeException("ClientError"));
  })
  .retrieve()
  .bodyToFlux(JsonNode.class)
  ...

6、总结

本文介绍了如何通过 Netty 为 WebClient 配置全局的连接超时、读写超时、响应超时、SSL/TLS 握手超时以及代理超时。还介绍了如何针对单个请求设置超时时间以及如何处理不同的超时异常。


Ref:https://www.baeldung.com/spring-webflux-timeout