在 multi-thread 环境中使用 Spring WebClient 的正确方法
Right way to use Spring WebClient in multi-thread environment
我有一个关于 Spring WebClient
的问题
在我的应用程序中,我需要执行许多类似的 API 调用,有时我需要在调用(身份验证令牌)中更改 headers。那么问题来了,这两个选项哪个更好:
要为 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吗? (因为服务被很多线程使用)
- 为每个传入服务的新请求创建新的 WebClient class。
我想提供最大的性能,并以正确的方式使用它,但我不知道 WebClient 在其中是如何工作的,以及它期望如何使用。
谢谢。
关于 WebClient
的两个关键问题:
- 它的 HTTP 资源(连接、缓存等)由底层库管理,由您可以在
WebClient
上配置的 ClientHttpConnector
引用
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() 来获取全局资源。您可以手动提供池设置,但考虑到即使使用默认设置也会出现的错误,我认为不值得冒险。
我有一个关于 Spring WebClient
的问题在我的应用程序中,我需要执行许多类似的 API 调用,有时我需要在调用(身份验证令牌)中更改 headers。那么问题来了,这两个选项哪个更好:
要为 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吗? (因为服务被很多线程使用)
- 为每个传入服务的新请求创建新的 WebClient class。
我想提供最大的性能,并以正确的方式使用它,但我不知道 WebClient 在其中是如何工作的,以及它期望如何使用。
谢谢。
关于 WebClient
的两个关键问题:
- 它的 HTTP 资源(连接、缓存等)由底层库管理,由您可以在
WebClient
上配置的 WebClient
是不可变的
ClientHttpConnector
引用
考虑到这一点,您应该尝试在您的应用程序中重用相同的 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() 来获取全局资源。您可以手动提供池设置,但考虑到即使使用默认设置也会出现的错误,我认为不值得冒险。