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

51

关于Spring Framework 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 是线程安全的吗?(因为服务被多个线程使用)
2. 对于每个进入服务类的新请求,创建一个新的 WebClient。
我想要发挥最大的性能并正确使用它,但是我不知道WebClient在内部是如何工作的,以及它希望如何被使用。
谢谢。

"WebClient 用于处理所有传入请求" --- 您是指 "传出" 请求吗? - Evvo
2个回答

89
关于WebClient有两个关键点:
  1. 它的HTTP资源(连接、缓存等)由底层库管理,可以在WebClient上配置的ClientHttpConnector引用该库。
  2. WebClient是不可变的。
考虑到这一点,您应该尽量在应用程序中重用相同的ClientHttpConnector,因为这将共享连接池 - 这对性能来说可能是最重要的事情。这意味着您应该尝试从同一个WebClient.create()调用派生所有WebClient实例。 Spring Boot通过创建和配置一个WebClient.Builder bean来帮助您,在应用程序的任何地方都可以注入它。
由于WebClient不可变的,所以它是线程安全的WebClient旨在在反应式环境中使用,其中没有任何内容与特定线程绑定(这并不意味着您不能在传统的Servlet应用程序中使用)。
如果您想改变请求的方式,有几种方法可以实现这一点:
在构建阶段配置事物。
WebClient baseClient = WebClient.create()
                                .baseUrl("https://example.org");

根据每个请求的需求进行配置事项。
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(hdrs -> {hdrs.add("token", "secret");})
                                 .build();

1
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web-reactive.html#webflux-client - Brian Clozel
2
我从未说过你应该为整个应用程序只使用一个Web客户端。我相信Reactor Netty的连接池足够聪明,并且是基于每个主机管理连接的,所以我不明白这会发生什么。与此同时,Spring Framework中我们管理HTTP资源的方式发生了变化 - 因此请提出一个新问题,并随时指向我。 - Brian Clozel
2
在多线程环境下,WebClient正在覆盖我的URI。WebClient是以以下方式在类级别上初始化的:private WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector((HttpClientOptions.Builder builder) -> builder.disablePool())).build();必须每个请求级别进行变异。如果这是正确的方法,请告诉我。 - Praveen Kamath
1
我已经在Stack Overflow上发布了一个问题,关于Spring Boot v2.0.0.M6 WebClient会发出多个重复的HTTP POST请求的问题。请帮忙! - Praveen Kamath
1
WebClient.Builder似乎不是线程安全的,那么如何获取WebClient呢?这里有我的问题,如果你愿意看一下:https://dev59.com/SVQJ5IYBdhLWcg3wT0Ay - P_M
显示剩余5条评论

13

根据我的经验,如果您在调用一个您无法控制的外部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();
}
***创建一个单独的WebClient并不意味着它会有一个单独的连接池。只需查看HttpClient.create的代码 - 它调用HttpResources.get()以获取全局资源。您可以手动提供池设置,但考虑到即使使用默认设置也可能发生错误,我认为这并不值得冒险。

根据文档所述,Httpclient.create()提供了一个池化资源。但我不明白为什么共享池会导致错误。当然,如果您订阅相应的流,那么处理错误的工作就由该流来完成了,对吧?为什么要建议人们不要使用呢?实际上,它非常适合进行并发的非阻塞多个请求。默认的每个连接池的请求数量是500。 - Deekshith Anand
一个随机错误不可能无缘无故地发生。要么是流通不当,要么是代码存在问题。除非明确指出导致错误的原因,否则这个答案可能会误导。@rougou - Deekshith Anand
2
是的,我应该澄清一下,我们使用的是阻塞模式,因此在我们的代码中根本不处理流通量。问题的提出方式让我觉得OP也只是想从几个请求线程中进行阻塞调用。在这种情况下,你唯一获得的好处就是连接池,但如果目标服务器不希望你池化连接,你可能会遇到随机错误,这就是我们所看到的。 - rougou
1
在非阻塞应用程序中,我同意WebClient是首选,并且在另一个项目中我也使用它。 - rougou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接