Spring Webflux with Spring MVC - 使用 doOnEach 时发出空值

Spring Webflux with Spring MVC - nulls emitted when using doOnEach

我正在尝试使用 Spring Webflux 和 Spring MVC,遇到一个有趣的案例。

从一个简单的控制器开始:

@GetMapping
public Mono<String> list(final Model model) {
    Flux<User> users = this.userRepository.findAll();
    model.addAttribute("users", users);
    return Mono.just("users/list");
}

userReposutory 是基于 ConcurrentHashMap 的自定义实现。在这里你可以找到 findAll 方法:

@Override
public Flux<User> findAll() {
    return Flux.fromIterable(this.users.values());
}

每当我尝试 return 访问 "users/list" 视图时,一切似乎都正常工作。

但是,如果我尝试使用惯用的反应式方法重写控制器,问题就会开始出现:

@GetMapping
public Mono<String> list(final Model model) {
    return this.userRepository.findAll()
      .collectList()
      .doOnEach(users -> model.addAttribute("users", users.get()))
      .map(u -> "users/list");
}

如果我到达终点,我会在日志中看到:

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[=15=](UserController.java:37)
    at reactor.core.publisher.FluxDoOnEach$DoOnEachSubscriber.onError(FluxDoOnEach.java:132)

显然,一些流浪者 null 正在前往那里。那就赶紧把它们都过滤掉吧:

@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");
}

同样的问题,但是...如果我在 map() 调用中压缩所有内容,一切都会再次运行:

@GetMapping
public Mono<String> list(final Model model) {
    return this.userRepository.findAll()
      .collectList()
      .map(users -> {
          model.addAttribute("users", users);
          return "users/list";
      });
}

不过,在 map 中放置副作用并不是最佳选择。

知道这里的 doOnEach() 有什么问题吗?

非常好的问题。让我们看看 JavaDocs 关于 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

很好奇。 doOnEach(users -> ...) 中的 users 不是 List<User>,而是 Signal<List<User>>。此 Signal<T> 对象不会为空,这解释了为什么第二个版本中的 filter 方法不起作用。

JavaDocs for Signal<T> 表示 get() 方法被显式标记为 @Nullable 并且 return 只有在下一个项目到达时才会有一个非空值。如果产生了完成或错误信号,那么它将return null.

解决方案:

  1. 改用doOnNext:您感兴趣的是下一个值,而不是来自源流的任何信号。
  2. doOnEach lambda 中进行空值检查:这也可以,但由于您对其他事件不感兴趣,因此是多余的。