Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mono switchIfEmpty() is always called

I have two methods.
Main method:

@PostMapping("/login") public Mono<ResponseEntity<ApiResponseLogin>> loginUser(@RequestBody final LoginUser loginUser) {     return socialService.verifyAccount(loginUser)             .flatMap(socialAccountIsValid -> {                 if (socialAccountIsValid) {                     return this.userService.getUserByEmail(loginUser.getEmail())                             .switchIfEmpty(insertUser(loginUser))                             .flatMap(foundUser -> updateUser(loginUser, foundUser))                             .map(savedUser -> {                                 String jwts = jwt.createJwts(savedUser.get_id(), savedUser.getFirstName(), "user");                                 return new ResponseEntity<>(HttpStatus.OK);                             });                 } else {                     return Mono.just(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));                 }             });  } 

And this invoked method (the service calls an external api):

public Mono<User> getUserByEmail(String email) {     UriComponentsBuilder builder = UriComponentsBuilder             .fromHttpUrl(USER_API_BASE_URI)             .queryParam("email", email);     return this.webClient.get()             .uri(builder.toUriString())             .exchange()             .flatMap(resp -> {                 if (Integer.valueOf(404).equals(resp.statusCode().value())) {                     return Mono.empty();                 } else {                     return resp.bodyToMono(User.class);                 }             }); }  

In the above example, switchIfEmpty() is always called from the main method, even when a result with Mono.empty() is returned.

I cannot find a solution for this simple problem.
The following also doesn't work:

Mono.just(null)  

Because the method will throw a NullPointerException.

What I also can't use is the flatMap method to check that foundUser is null.
Sadly, flatMap doesn't get called at all in case I return Mono.empty(), so I cannot add a condition here either.

@SimY4

   @PostMapping("/login")     public Mono<ResponseEntity<ApiResponseLogin>> loginUser(@RequestBody final LoginUser loginUser) {         userExists = false;         return socialService.verifyAccount(loginUser)                 .flatMap(socialAccountIsValid -> {                     if (socialAccountIsValid) {                         return this.userService.getUserByEmail(loginUser.getEmail())                                 .flatMap(foundUser -> {                                     return updateUser(loginUser, foundUser);                                 })                                 .switchIfEmpty(Mono.defer(() -> insertUser(loginUser)))                                 .map(savedUser -> {                                     String jwts = jwt.createJwts(savedUser.get_id(), savedUser.getFirstName(), "user");                                     return new ResponseEntity<>(HttpStatus.OK);                                 });                     } else {                         return Mono.just(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));                     }                 });      } 
like image 878
html_programmer Avatar asked Jan 25 '19 23:01

html_programmer


People also ask

What is mono switchIfEmpty?

The switchIfEmpty allows you to provide an alternative Mono as a fallback when the current Mono completes empty whereas defaultIfEmpty assigns a default value on empty Mono. Let's see an example to understand the differences.

How do you know if Mono is empty?

Default value if mono is empty. If you want to provide a default value when the mono is completed without any data, then use defaultIfEmpty method. For example, the following code tries to fetch the customer data from the database by using the customer id .

What is mono subscribe?

Mono subscribe() The subscribe() method with no arguments subscribes to the Mono and requests for the data from the publisher. It does not consume the data and also has no error handling mechanism.

What does mono just do?

just method is the simplest method for Mono generation. It takes a single value and generates a finite Mono stream from it. A completion event is published after publishing the specified value: Mono.


2 Answers

It's because switchIfEmpty accepts Mono "by value". Meaning that even before you subscribe to your mono, this alternative mono's evaluation is already triggered.

Imagine a method like this:

Mono<String> asyncAlternative() {     return Mono.fromFuture(CompletableFuture.supplyAsync(() -> {         System.out.println("Hi there");         return "Alternative";     })); } 

If you define your code like this:

Mono<String> result = Mono.just("Some payload").switchIfEmpty(asyncAlternative()); 

It'll always trigger alternative no matter what during stream construction. To address this you can defer evaluation of a second mono by using Mono.defer

Mono<String> result = Mono.just("Some payload")         .switchIfEmpty(Mono.defer(() -> asyncAlternative())); 

This way it will only print "Hi there" when alternative is requested

UPD:

Elaborating a little on my answer. The problem you're facing is not related to Reactor but to Java language itself and how it resolves method parameters. Let's examine the code from the first example I provided.

Mono<String> result = Mono.just("Some payload").switchIfEmpty(asyncAlternative()); 

We can rewrite this into:

Mono<String> firstMono = Mono.just("Some payload"); Mono<String> alternativeMono = asyncAlternative(); Mono<String> result = firstMono.switchIfEmpty(alternativeMono); 

These two code snippets are semantically equivalent. We can continue unwrapping them to see where the problem lies:

Mono<String> firstMono = Mono.just("Some payload"); CompletableFuture<String> alternativePromise = CompletableFuture.supplyAsync(() -> {         System.out.println("Hi there");         return "Alternative";     }); // future computation already tiggered Mono<String> alternativeMono = Mono.fromFuture(alternativePromise); Mono<String> result = firstMono.switchIfEmpty(alternativeMono); 

As you can see future computation was already triggered at the point when we start composing our Mono types. To prevent unwanted computations we can wrap our future into a defered evaluation:

Mono<String> result = Mono.just("Some payload")         .switchIfEmpty(Mono.defer(() -> asyncAlternative())); 

Which will unwrap into

Mono<String> firstMono = Mono.just("Some payload"); Mono<String> alternativeMono = Mono.defer(() -> Mono.fromFuture(CompletableFuture.supplyAsync(() -> {         System.out.println("Hi there");         return "Alternative";     }))); // future computation defered Mono<String> result = firstMono.switchIfEmpty(alternativeMono); 

In second example the future is trapped in a lazy supplier and is scheduled for execution only when it will be requested.

like image 70
SimY4 Avatar answered Sep 20 '22 01:09

SimY4


For those who, despite the well voted answer, do not still understand why such a behaviour:

Reactor sources (Mono.xxx & Flux.xxx) are either:

  • Lazily evaluated : the content of the source is evaluated/triggered only when a subscriber subscribes to it;

  • or eagerly evaluated : the content of the source is immediately evaluated even before the subscriber subscribes.

Expressions like Mono.just(xxx), Flux.just(xxx), Flux.fromIterable(x,y,z) are eager.

By using defer(), you force the source to be lazily evaluated. That's why the accepted answer works.

So doing this:

 someMethodReturningAMono()   .switchIfEmpty(buildError());  

with buildError() relying on an eager source to create an alternative Mono will ALWAYS be evaluated before the subscription:

Mono<String> buildError(){        return Mono.just("An error occured!"); //<-- evaluated as soon as read }  

To prevent that, do this:

 someMethodReturningAMono()   .switchIfEmpty(Mono.defer(() -> buildError()));  

Read this answer for more.

like image 43
Philippe Simo Avatar answered Sep 19 '22 01:09

Philippe Simo