在 multi-thread 环境中使用 Spring WebClient 的正确方法

Right way to use Spring WebClient in multi-thread environment

我有一个关于 Spring WebClient

的问题

在我的应用程序中,我需要执行许多类似的 API 调用,有时我需要在调用(身份验证令牌)中更改 headers。那么问题来了,这两个选项哪个更好:

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

    private final WebClient webClient = WebClient.builder()
            .baseUrl("<a href="https://another_host.com/api/get_inf" rel="noreferrer">https://another_host.com/api/get_inf</a>")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .build();
    </pre>

这里又出现一个问题:WebClient是thread-safe吗? (因为服务被很多线程使用)

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

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

谢谢。

关于 WebClient 的两个关键问题:

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

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

因为 WebClient 是不可变的所以它是 thread-safe。 WebClient 旨在用于反应式环境,其中没有任何东西与特定线程相关联(这并不意味着您不能在传统的 Servlet 应用程序中使用)。

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

在构建阶段配置东西

WebClient baseClient = WebClient.create().baseUrl("https://example.org");

在 per-request 基础上配置东西

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

从现有的实例中创建一个新的客户端实例

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

根据我的经验,如果您在无法控制的服务器上调用外部 API,则根本不要使用 WebClient,或者在关闭池机制的情况下使用它。连接池带来的任何性能提升都被(默认的 reactor-netty)库中内置的假设大大超过了,当另一个 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();
}

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