I'm experimenting a bit with Spring Webflux and Spring MVC, and encountered an interesting case.
Starting with a simple controller:
@GetMapping
public Mono<String> list(final Model model) {
Flux<User> users = this.userRepository.findAll();
model.addAttribute("users", users);
return Mono.just("users/list");
}
The userReposutory
is a custom ConcurrentHashMap
-based implementation. Here you can find the findAll
method:
@Override
public Flux<User> findAll() {
return Flux.fromIterable(this.users.values());
}
Whenever I try to return to access the "users/list" view, everything seems to be working properly.
But, if I try to rewrite the controller using an idiomatic reactive approach, problems start appearing:
@GetMapping
public Mono<String> list(final Model model) {
return this.userRepository.findAll()
.collectList()
.doOnEach(users -> model.addAttribute("users", users.get()))
.map(u -> "users/list");
}
If I hit the endpoint, I'm getting this in logs:
java.lang.IllegalArgumentException: ConcurrentModel does not support null attribute value
at org.springframework.util.Assert.notNull(Assert.java:193)
at org.springframework.ui.ConcurrentModel.addAttribute(ConcurrentModel.java:75)
at org.springframework.ui.ConcurrentModel.addAttribute(ConcurrentModel.java:39)
at com.baeldung.lss.web.controller.UserController.lambda$list$0(UserController.java:37)
at reactor.core.publisher.FluxDoOnEach$DoOnEachSubscriber.onError(FluxDoOnEach.java:132)
Apparently, some stray null
is making its way there. Let's filter out all of them eagerly then:
@RequestMapping
public Mono<String> list(final Model model) {
return this.userRepository.findAll()
.filter(Objects::nonNull)
.collectList()
.filter(Objects::nonNull)
.doOnEach(users -> model.addAttribute("users", users.get()))
.map(u -> "users/list");
}
Same problem, but... if I squeeze everything in a map()
call, everything works again:
@GetMapping
public Mono<String> list(final Model model) {
return this.userRepository.findAll()
.collectList()
.map(users -> {
model.addAttribute("users", users);
return "users/list";
});
}
Although, placing side-effects in map
is not optimal.
Any ideas what's wrong with the doOnEach()
here?
both infrastructure will compete for the same job (for example, serving static resources, the mappings, etc) mixing both runtime models within the same container is not a good idea and is likely to perform badly or just not work at all.
Webflux InternalsReactor Netty is an asynchronous, event-driven network application framework built out of Netty server which provides non-blocking and backpressure-ready network engines for HTTP, TCP, and UDP clients and servers.
The main difference between the two frameworks is that spring-mvc is based on thread pools, while spring-webflux is based on event-loop mechanism. Both the models support commonly used annotations such as @Controller . A developer can run a reactive client from a spring-mvc controller to make calls to remote services.
What is Spring WebFlux ? Spring WebFlux is parallel version of Spring MVC and supports fully non-blocking reactive streams. It support the back pressure concept and uses Netty as inbuilt server to run reactive applications. If you are familiar with Spring MVC programming style, you can easily work on webflux also.
Very nice question. Let's see what the JavaDocs tell about doOnEach
:
public final Mono<T> doOnEach(Consumer<? super Signal<T>> signalConsumer)
Add behavior triggered when the Mono emits an item, fails with an error or completes successfully. All these events are represented as a
Signal
that is passed to the side-effect callback
Curious. The users
in doOnEach(users -> ...)
is not an List<User>
but a Signal<List<User>>
. This Signal<T>
object won't be null, which explains why the filter
methods in the second version don't work.
The JavaDocs for Signal<T>
says that the get()
method is explicitly marked as @Nullable
and will return a non-null value only on next item arrives. If the completion or error signal is generated, then it will return null
.
Solutions:
doOnNext
instead: You are interested in the next value, not any signal that comes from the source stream. doOnEach
lambda: This will work too, but since you're not interested in other events, is superfluous.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With