Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is Spring boot WebClient real asynchronous?

WebClient is a reactive client that provides an alternative to RestTemplate. It is said that it's asynchronous.

But I doubt it with below code:

 WebClient.create()
            .method(HttpMethod.GET)
            .uri("http://localhost:8080/testApi")
            .retrieve()
            .bodyToMono(String.class)

It does nothing. Not any http request is sent. It seems it isn't triggerd. Unless I trigger it by add .block(). But it make things not 'Async'.

Also,What I know is using .subscribe() that make things looks async.

But is the WebClient designed for this? What's the best pratices to use WebClient.

like image 771
roast_soul Avatar asked May 16 '26 16:05

roast_soul


2 Answers

WebClient uses Reactor Netty to provide a fully asynchronous, NIO networking library for Java.

Using .block(), you'll be blocking the calling thread, which is not desired. For I/O calls, you should use .flatMap, which subscribes to the inner streams and dynamically merges the results as and when they arrive. For you example, I would do:

Mono.just(httpRequest)
    .flatMap(request -> request.exchange().bodyToMono(String.class))
    .map(response -> doWhateverToTheResponse(response))
    .subscribeOn(Schedulers.elastic())
    .subscribe();

Note that .flatMap() will now subscribe to the inner stream. When the response comes back, it'll go into the map and the stream will continue. The calling thread would have become free as the stream is started on an elastic thread.

Never use .block() unless it is absolutely required. One use-case for using it: A polling loop of Kafka. You want to ensure that you have processed a set of records before your loop consumes the next set of records. .block() ensures that the polling thread remains blocked unless all the records are processed.

Edit: I wrote a small article on .flatMap() a few months back. I think it's pretty well written, you can have a look here:

https://medium.com/swlh/understanding-reactors-flatmap-operator-a6a7e62d3e95

like image 59
Prashant Pandey Avatar answered May 18 '26 05:05

Prashant Pandey


Yes, but the confusion is understandable. You'll get some shade from a less-constructive segment of the reactive community for framing the question as you did, but as one who came to reactive after working in NodeJS, which is async to it's very core (and did some Twisted before that) the reactive interfaces are a bit clunky, the documentation is often poorly written (but getting much better), and the defaults will usually surprise the heck out of you.

A couple starter hints, in no particular order:

  • at the end of the day, it's all a wrapper around Netty, which is a very solid, well-maintained, well-documented, very nice library. At some point during load testing you may see bizarre queue exceptions referencing Netty, etc, and it may not hurt to read up on it: https://netty.io/ . Side note: spring often does this - wraps up a solid piece of tech developed elsewhere in a bunch of confusing new terms and clunky, quickly deprecated interfaces

  • Skip most rants about backpressure, etc, and pick up a copy of "Designing Data-Intensive Applications" by Martin Kleppman for a far more cogent and nuanced look at how to build robust, asynchronous message-passing systems. In reactive, "backpressure" usually means "your in-memory queue is full, and now you're .... ?" Where the question mark is a callback and/or you swearing.

  • Reactive defaults to running on the current thread.. which can lead to strange results (including blocking the program). When it doubt, use subscribeOn( Schedulers.boundedElastic ). Once you wrap your head around the whole scheduler mess, publishOn(..) is a little more targeted.

  • "Operators". Spring likes to rename things. It's yet another word for "callback".

  • web calls, as you noted, are (effectively) evaluated lazily. Nothing happens until you subscribe / block. Which makes it incredibly annoying to fire off a request, do some CPU work, fire off a different request, do some CPU work, etc.. and then, at some point, check in on all the requests, merge the data, and move on. People will pitch things like zip() at you, but it's not a great approach, because it's messy to get the "some CPU work" running in parallel with the requests. At best you can zip into some callback hell, if you go down that route. It's often easier to fall back onto CompletableFutures, which have a much cleaner interface.

  • Fluxes don't run in parallel automatically. Which is just silly, but you gotta call parallel() AND runOn()

  • WebFlux mostly uses Monos, not Fluxes. Go figure.

  • handling errors & harvesting response codes in WebFlux is a mess, and there are a few different ways, none good, all with opinionated sides. I favor exchangeToMono().

  • Mono's are Deferred / Futures /etc with limited functionality that might actually execute in the foreground if you're not careful.

        CompletableFuture<Optional<ReturnType>> response = new CompletableFuture<>();
        client.post()
             .uri("why isn't this an argument to post???")
             .bodyValue("register a ridiculous number of callbacks to make a web call")
             .exchangeToMono(this::handleResponse)
             .subscribeOn(Schedulers.boundedElastic())
             .subscribe(resp -> response.complete(Optional.of(resp))
        ///Do some CPU work
        // make another request
        response.get(...bound the response with some time & wrap in exception handlers, if you like...)
             

like image 25
user2077221 Avatar answered May 18 '26 04:05

user2077221



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!