16

I would like to have a http client to call other microservice from Spring Boot not reactive application. Because of RestTemplate will be deprecated I tried to use WebClient.Builder and WebClient. Though I not sure about thread safety. Here example:

@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;
    }

}

The serviceMethod() method in this example will be called from few threads, and webClientBuilder is a single bean instance. The WebClient.Builder class contains state: baseUrl, and this seems not thread safe as few threads could call this state update simultaneously.

Meanwhile WebClient itself seems is thread safe as mentioned in answer at:

Should I use WebClient.Builder bean as mentioned at the WebClient section as follows:

Spring Boot creates and pre-configures a WebClient.Builder for you; it is strongly advised to inject it in your components and use it to create WebClient instances.

One of workaround options I see is to create WebClient without any state passed to builder so instead of:

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

I will do:

WebClient webClient = webClientBuilder.build();

and pass full url with protocol and port in uri method call:

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

What is the proper way to use it in my case?

Manuel Jordan
  • 15,253
  • 21
  • 95
  • 158
P_M
  • 2,723
  • 4
  • 29
  • 62

1 Answers1

24

You're right, WebClient.Builder is not thread-safe.

Spring Boot is creating WebClient.Builder as a prototype bean, so you'll get a new instance for each injection point. In your case, your component seems a bit strange in my opinion.

It should rather look like this:

@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;
    }
}

Now I guess this is a code snippet and your application may have different constraints.

If your application needs to change the base URL often, then I think you should stop configuring it on the builder and pass the full URL as mentioned in your question. If your application has other needs (custom headers for authentication, etc), then you can also do that on the builder or on a per request basis.

In general, you should try and build a single WebClient instance per component, as recreating it for each request is quite wasteful.

In case your application has very specific constraints and really needs to create different instances, then you can always call webClientBuilder.clone() and get a new instance of the builder that you can mutate, without the thread safety issues.

Manuel Jordan
  • 15,253
  • 21
  • 95
  • 158
Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
  • 2
    My @Service will need to talk to few microservices over http. I know their functionality, but addressees could change at run time. So I think I can get WebClient.Builder injected in constructor and build my WebClient instance without baseUrl call in WebClient.Builder. It seems in this case I need just one WebClient instance for all remote microservices. Right? – P_M Jan 11 '19 at 10:07
  • Yes; baseUrl is just a convenience in case all requests will be sent to the same host. This is not your case so you should drop that from the builder phase and just issue your requests with the full URLs – Brian Clozel Jan 11 '19 at 10:09
  • Just checked WebClient.Builder is prototype bean. I will go with route you proposed. I was not able to find all answers to my question at https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-webclient.html and I tried to use builder in my service method, because thought initially WebClient is not thread safe (while it seems actually is). Thank you! – P_M Jan 11 '19 at 10:34
  • Where do you have the information from that Spring Boot is creating WebClient.Builder as a prototype bean? I just debugged an application with 2 clients and the same builder instance gets injected! I'm now looking for a way to solve this... – Puce Jun 21 '19 at 15:18
  • Here it the link for the auto-configuration: https://github.com/spring-projects/spring-boot/blob/c7d3b7a9f1b2614d56a105a80fef40baa7c3111a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java#L60 If you've got a sample application showing this issue, you can submit an issue on the Spring Boot project. – Brian Clozel Jun 21 '19 at 15:20
  • @BrianClozel It was a misconfiguration in the application. So it was never hitting WebClientAutoConfiguration. Sorry for the confusion and thanks again for the hint in the right direction. – Puce Jun 21 '19 at 15:34