I am trying to write a reactive function that performs a certain task when the first Mono is non-empty, and a "fallback" task when the Mono is empty. This is what I have so far:
static Mono<String> init(String input) {
if (input == null) {
return Mono.empty();
}
if ("err".equals(input)) {
throw new IllegalArgumentException("Some exception");
}
System.out.println("First Mono: " + input);
return Mono.just("First Mono: " + input);
}
static Mono<Void> doesSomeStuff(String input) {
System.out.println("Second Mono: " + input);
return Mono.empty();
}
My first attempt was to specify a flatMap
call that would handle the valid Mono scenario, and a switchIfEmpty
call that would handle the empty Mono scenario
/**
* Wrong! The 3rd line should not be shown! Defer causes empty block to be lazily evaluated
*
* First Mono: Hello
* Second Mono: First Mono: Hello
* Second Mono: Empty
*/
Mono<String> firstMono = init("Hello");
firstMono.flatMap(WebfluxDemoApplication::doesSomeStuff)
.switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")))
.block();
/**
* Correct! Only the empty branch is executed
*
* Second Mono: Empty
*/
Mono<String> first2Mono = init(null);
first2Mono.flatMap(WebfluxDemoApplication::doesSomeStuff)
.switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")))
.block();
However, the output does not align with my expectations. I did some searching and I did find a sequence of operations that DOES work:
/**
* Correct!
*
* First Mono: Hello
* Second Mono: First Mono: Hello
*/
Mono<String> first5Mono = init("Hello");
first5Mono.delayUntil(initVal -> doesSomeStuff(initVal))
.switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")).cast(String.class))
.then()
.block();
System.out.println("---");
/**
* Correct!
*
* Second Mono: Empty
*/
Mono<String> first6Mono = init(null);
first6Mono.delayUntil(initVal -> doesSomeStuff(initVal))
.switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")).cast(String.class))
.then()
.block();
System.out.println("---");
/**
* Correct! It throws the Exception.
*
* Second Mono: Empty
*/
Mono<String> first7Mono = init("err");
first7Mono.delayUntil(initVal -> doesSomeStuff(initVal))
.switchIfEmpty(Mono.defer(() -> doesSomeStuff("Empty")).cast(String.class))
.then()
.block();
Can someone explain to me why this works? Why does delayUntil
work, but not flatMap
? Why do I have to cast the Mono in the switchIfEmpty
statement? I believe then
is used to signal completion by emitting an empty Mono. Does this just work coincidentally, because doesSomeStuff
return type is Mono<Void>
?
flatMap
doesn't work because you are always flat-mapping to an empty Mono
. This means it will always return an empty Mono
, causing switchIfEmpty
to always "switch".
delayUntil
doesn't affect the original Mono
's value in any way. It just makes it emit its value a little later.
I do think flatMap
is more readable though, so you could do something like this:
mono.flatMap(x -> doesSomeStuff(x).thenReturn(x))
.switchIfEmpty(Mono.defer(() -> doesSomeStuff("empty")).then(Mono.empty()))
.block();
thenReturn
makes sure that flatMap
doesn't "eat up" the value. then(Mono.empty())
is then used to convert the Mono<Void>
into an empty Mono<String>
.
Your cast
works here because doesSomeStuff
always returns an empty Mono
. I suppose you could call that a "coincidence". then()
is not really relevant here - it just gets rid of the String
value that you would have gotten if the original Mono
does have a value. If doesSomeStuff
returns a non-empty Mono
, you'd need some other way to make it a Mono<String>
, e.g. then(Mono.empty())
.
That said, if doesSomeStuff
does return a non-empty Mono
of another type, you can just flatMap
and then switchIfEmpty
like you did in your first attempt. In this case, flatMap
will preserve the existence of a value.
If you don't actually need the value of the original Mono
to pass to doesSomeStuff
, you can just use hasElement
:
mono.hasElement().flatMap( hasElem ->
hasElem ? doesSomeStuff("intVal") : doesSomeStuff("Empty")
).block();