Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Webflux with Spring MVC - nulls emitted when using doOnEach

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?

like image 679
Grzegorz Piwowarek Avatar asked May 13 '18 09:05

Grzegorz Piwowarek


People also ask

Can I use Spring MVC and WebFlux together?

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.

Is WebFlux asynchronous?

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.

What is the difference between Spring MVC and Spring WebFlux?

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.

How does Spring WebFlux work internally?

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.


1 Answers

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:

  1. Use doOnNext instead: You are interested in the next value, not any signal that comes from the source stream.
  2. Do a null-check in doOnEach lambda: This will work too, but since you're not interested in other events, is superfluous.
like image 183
MuratOzkan Avatar answered Sep 19 '22 14:09

MuratOzkan