在多线程环境中使用Spring WebClient的正确方法

Ser*_*hko 19 spring spring-webflux

我有一个关于Spring WebClient的问题

在我的应用程序中,我需要执行许多类似的API调用,有时我需要在调用中更改标头(身份验证令牌).所以问题出现了,两种选择会更好:

  1. 要为MyService.class的所有传入请求创建一个WebClient,通过使其成为private final字段,如下面的代码:

    private final WebClient webClient = WebClient.builder()
            .baseUrl("https://another_host.com/api/get_inf")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .build();
    

这里出现了另一个问题:WebClient线程安全吗?(因为服务被许多线程使用)

  1. 为传入服务类的每个新请求创建新的WebClient.

我想提供最大的性能,并以正确的方式使用它,但我不知道WebClient如何在其中工作,以及它如何使用.

谢谢.

Bri*_*zel 24

这里有两个关键的事情WebClient:

  1. 它的HTTP资源(连接,缓存等)由底层库管理,由ClientHttpConnector您可以在其上配置WebClient
  2. WebClient 是不可改变的

考虑到这一点,您应该尝试在ClientHttpConnector整个应用程序中重用相同的内容,因为这将共享连接池 - 这可能是性能最重要的事情.这意味着您应该尝试WebClient从同一个WebClient.create()调用派生所有实例.Spring Boot通过为您创建和配置WebClient.Builder可以在应用程序中的任何位置注入的bean来帮助您.

因为WebClient它是不可变的,所以它是线程安全的.WebClient意味着在被动环境中使用,其中没有任何东西与特定线程相关联(这并不意味着您不能在传统的Servlet应用程序中使用).

如果您想更改请求的方式,有几种方法可以实现:

在构建阶段配置东西

WebClient baseClient = WebClient.create().baseUrl("https://example.org");
Run Code Online (Sandbox Code Playgroud)

根据请求配置内容

Mono<ClientResponse> response = baseClient.get().uri("/resource")
                .header("token", "secret").exchange();
Run Code Online (Sandbox Code Playgroud)

从现有客户端实例创建新客户端实例

// mutate() will *copy* the builder state and create a new one out of it
WebClient authClient = baseClient.mutate()
                .defaultHeaders(headers -> {headers.add("token", "secret");})
                .build();
Run Code Online (Sandbox Code Playgroud)

  • 我从未说过整个应用程序应该只有一个网络客户端。我相信 Reactor Netty 的连接池足够聪明,并且在每个主机的基础上管理连接,所以我不明白这是如何发生的。与此同时,Spring 框架中我们管理 HTTP 资源的方式发生了变化——所以请提出一个新问题并随时向我指出。 (2认同)
  • 多线程环境中的 WebClient 正在覆盖我的 URI。WebClient 以下列方式在类级别初始化 private WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector((HttpClientOptions.Builder builder) -&gt; builder.disablePool())).build(); 必须根据请求级别对其进行变异。让我知道这是否是正确的方法。 (2认同)

rou*_*gou 14

根据我的经验,如果您在无法控制的服务器上调用外部 API,则根本不要使用 WebClient,或者在关闭池机制的情况下使用它。连接池带来的任何性能提升都远远超过了(默认的reactor-netty)库中内置的假设,当远程主机突然终止另一个API调用时,该假设将导致一个API调用出现随机错误。有时,您甚至不知道错误发生在哪里,因为所有调用都是从共享工作线程进行的。

我错误地使用了WebClient,因为文档说它RestTemplate将来会被弃用。事后看来,我会使用常规 HttpClient 或 Apache Commons HttpClient,但如果您像我一样并且已经使用 WebClient 实现,则可以通过创建 WebClient 来关闭池,如下所示:

private WebClient createWebClient(int timeout) {
    TcpClient tcpClient = TcpClient.newConnection();
    HttpClient httpClient = HttpClient.from(tcpClient)
        .tcpConfiguration(client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeout * 1000)
            .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(timeout))));

    return WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
}
Run Code Online (Sandbox Code Playgroud)

创建单独的 WebClient 并不意味着 WebClient 将具有单独的连接池。只需查看代码即可HttpClient.create- 它调用HttpResources.get()获取全局资源。您可以手动提供池设置,但考虑到即使使用默认设置也会发生的错误,我认为不值得冒这个风险。

  • @Deekshith Anand 是的,我应该澄清我们正在使用阻塞模式,因此根本不处理代码中的通量流。提出问题的方式让我假设OP也只是想从多个请求线程进行阻塞调用。在这种情况下,您获得的唯一好处是连接池,但如果目标服务器不希望您池化连接,则最终可能会出现随机错误,这就是我们所看到的。 (2认同)