Spring Boot WebClient.Builder bean 在传统 servlet 多线程应用程序中的使用

Posted

技术标签:

【中文标题】Spring Boot WebClient.Builder bean 在传统 servlet 多线程应用程序中的使用【英文标题】:Spring Boot WebClient.Builder bean usage in traditional servlet multi threaded application 【发布时间】:2019-06-05 18:14:01 【问题描述】:

我希望有一个 http 客户端从 Spring Boot not reactive 应用程序中调用其他微服务。由于 RestTemplate 将被弃用,我尝试使用 WebClient.Builder 和 WebClient。虽然我不确定线程​​安全。这里的例子:

@Service
public class MyService
    @Autowired
    WebClient.Builder webClientBuilder;

    public VenueDTO serviceMethod()
        //!!! This is not thread safe !!!
        WebClient webClient = webClientBuilder.baseUrl("http://localhost:8000").build();

        VenueDTO venueDTO = webClient.get().uri("/api/venue/id", bodDTO.getBusinessOutletId()).
                retrieve().bodyToMono(VenueDTO.class).
                blockOptional(Duration.ofMillis(1000)).
                orElseThrow(() -> new BadRequestException(venueNotFound));
                return VenueDTO;
    

本示例中的serviceMethod() 将从几个线程中调用,webClientBuilder 是单个bean 实例。 WebClient.Builder 类包含状态:baseUrl,这似乎不是线程安全的,因为很少有线程可以同时调用此状态更新。同时,WebClient 本身似乎是线程安全的,正如Right way to use Spring WebClient in multi-thread environment 的回答中提到的那样

我应该使用https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-webclient.html 中提到的 WebClient.Builder bean

Spring Boot 为您创建和预配置一个 WebClient.Builder;它 强烈建议将其注入您的组件中并用于 创建 WebClient 实例。

我看到的解决方法之一是创建 WebClient 而不将任何状态传递给构建器,而不是:

WebClient webClient = webClientBuilder.baseUrl("http://localhost:8000").build();

我会的:

WebClient webClient = webClientBuilder.build();

并在 uri 方法调用中传递带有协议和端口的完整 url:

webClient.get().uri("full url here", MyDTO.class)

在我的情况下使用它的正确方法是什么?

【问题讨论】:

【参考方案1】:

你说得对,WebClient.Builder 不是线程安全的。

Spring Boot 正在创建 WebClient.Builder 作为原型 bean,因此您将为每个注入点获得一个新实例。就您而言,我认为您的组件似乎有点奇怪。

它应该看起来像这样:

@Service
public class MyService

    private final WebClient webClient;

    public MyService(WebClient.Builder webClientBuilder) 
        this.webClient = webClientBuilder.baseUrl("http://localhost:8000").build();
    

    public VenueDTO serviceMethod()
        VenueDTO venueDTO = webClient.get().uri("/api/venue/id", bodDTO.getBusinessOutletId()).
                retrieve().bodyToMono(VenueDTO.class).
                blockOptional(Duration.ofMillis(1000)).
                orElseThrow(() -> new BadRequestException(venueNotFound));
                return VenueDTO;
    

现在我猜这是一个代码 sn-p 并且您的应用程序可能有不同的约束。

如果您的应用程序需要经常更改基本 URL,那么我认为您应该停止在构建器上配置它并传递问题中提到的完整 URL。如果您的应用程序有其他需求(用于身份验证的自定义标头等),那么您也可以在构建器或基于每个请求的基础上执行此操作。

一般来说,您应该尝试为每个组件构建一个 WebClient 实例,因为为每个请求重新创建它非常浪费。

如果您的应用程序有非常具体的约束并且确实需要创建不同的实例,那么您可以随时调用 webClientBuilder.clone() 并获取一个可以变异的构建器新实例,而不会出现线程安全问题。

【讨论】:

我的@Service 需要通过 http 与几个微服务通信。我知道它们的功能,但收件人可能会在运行时更改。所以我认为我可以在构造函数中注入 WebClient.Builder 并构建我的 WebClient 实例,而无需在 WebClient.Builder 中调用 baseUrl。在这种情况下,所有远程微服务似乎只需要一个 WebClient 实例。对吗? 是的; baseUrl 只是一种方便,以防所有请求都发送到同一主机。这不是您的情况,因此您应该从构建器阶段删除它,并使用完整的 URL 发出您的请求 刚刚检查 WebClient.Builder 是原型 bean。我会选择你建议的路线。我无法在docs.spring.io/spring-boot/docs/current/reference/html/… 找到我的问题的所有答案,我尝试在我的服务方法中使用构建器,因为最初认为 WebClient 不是线程安全的(虽然它看起来确实是)。谢谢! 您从哪里获得 Spring Boot 正在创建 WebClient.Builder 作为原型 bean 的信息?我刚刚调试了一个带有 2 个客户端的应用程序,并且注入了同一个构建器实例!我现在正在寻找解决这个问题的方法... 这里是自动配置的链接:github.com/spring-projects/spring-boot/blob/… 如果你有一个显示这个问题的示例应用程序,你可以在 Spring Boot 项目上提交一个问题。

以上是关于Spring Boot WebClient.Builder bean 在传统 servlet 多线程应用程序中的使用的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Spring Boot 应用程序 pom 同时需要 spring-boot-starter-parent 和 spring-boot-starter-web?

《02.Spring Boot连载:Spring Boot实战.Spring Boot核心原理剖析》

spring-boot-quartz, 依赖spring-boot-parent

spring-boot系列:初试spring-boot

Spring Boot:Spring Boot启动原理分析

Spring Boot:Spring Boot启动原理分析