Java Reactor 中嵌套 flatMaps 的好习惯是啥?

Posted

技术标签:

【中文标题】Java Reactor 中嵌套 flatMaps 的好习惯是啥?【英文标题】:What is a good idiom for nested flatMaps in Java Reactor?Java Reactor 中嵌套 flatMaps 的好习惯是什么? 【发布时间】:2020-01-22 23:53:47 【问题描述】:

我继承了使用 Spring 和包括 Reactor 在内的相关库用 Java 编写的 REST 服务的责任。对于 REST 调用或数据库操作等昂贵的操作,代码将结果广泛包装在 Reactor Mono 中。

代码中有各种各样的事情需要解决,但不断出现的事情是嵌套在 flatMaps 上 Monos 用于一系列昂贵的操作,这些操作最终会缩进几个层次,导致无法阅读混乱。我觉得这很烦人,因为我来自 Scala,这种使用 flatMap 的方式并没有那么糟糕,因为 for comprehension 语法糖将所有内容保持在大致相同的范围内,而不是更深入。

到目前为止,除了大规模重构之外,我还没有成功找到一种方法来使其更具可读性(即使这样我也不确定从哪里开始这样的重构)。

基于代码的匿名示例,(所有语法错误均来自匿名):

public Mono<OutputData> userActivation(InputData input) 
    Mono<DataType1> d1 = service.expensiveOp1(input);

    Mono<OutputData> result =
        d1
          .flatMap(
            d1 -> 
              return service
                  .expensiveOp2(d1.foo())
                  .flatMap(
                      d2 -> 
                        if (Status.ACTIVE.equals(d2.getStatus())) 
                          throw new ConflictException("Already active");
                        

                        return service
                            .expensiveOp3(d1.bar(), d2.baz())
                            .flatMap(
                                d3 -> 
                                  d2.setStatus(Status.ACTIVE);

                                  return service
                                      .expensiveOp5(d1, d2, d3)
                                      .flatMap(
                                          d4 -> 
                                            return service.expensiveOp6(d1, d4.foobar())
                                          );
                                );
                      );
            )

    return result;


【问题讨论】:

【参考方案1】:

哎呀。有几件事我不喜欢那个 sn-p,但我将从大的开始 - 嵌套。

嵌套的唯一原因是,在(例如)expensiveOp5() 中,您需要引用 d1d2d3,而不仅仅是 d4 - 所以您不能只映射“通常”,因为您丢失了那些较早的引用。有时可以在特定上下文中重构这些依赖关系,因此我会先检查该路线。

但是,如果不可能或不可取,我倾向于发现像这样的深层嵌套 flatMap() 调用最好通过组合替换为中间对象。

如果你有一堆类,例如:

@Data
class IntermediateResult1 
    private DataType1 d1;
    private DataType2 d2;


@Data
class IntermediateResult2 
    public IntermediateResult2(IntermediateResult1 i1, DataType3 d3) 
        this.d1 = i1.getD1();
        this.d2 = i1.getD2();
        this.d3 = d3;
    
    private DataType1 d1;
    private DataType2 d2;
    private DataType3 d3;

...等等,然后您可以执行以下操作:

    return d1.flatMap(d1 -> service.expensiveOp2(d1.foo()).map(d2 -> new IntermediateResult1(d1, d2)))
             .flatMap(i1 -> service.expensiveOp3(i1).map(s3 -> new IntermediateResult2(i1, d3)))
             //etc.

当然,您也可以将调用分解为自己的方法以使其更清晰(在这种情况下我可能会建议这样做):

return d1.flatMap(this::doOp1)
         .flatMap(this::doOp2)
         .flatMap(this::doOp3)
         .flatMap(this::doOp4)
         .flatMap(this::doOp5);

显然,我在上面使用的名称应该只是占位符 - 你应该仔细考虑这些名称,因为这里的良好命名将使对反应流的推理和解释更加自然。

除了嵌套之外,该代码中还有两点值得注意:

使用return Mono.error(new ConflictException("Already active")); 而不是显式抛出,因为这样可以更清楚地表明您正在处理流中的显式Mono.error永远不要在反应链的中途使用可变方法,例如 setStatus() - 这会在以后提出问题。相反,使用with pattern 之类的东西来生成带有更新字段的d2 的新实例。然后,您可以致电 expensiveOp5(d1, d2.withStatus(Status.ACTIVE), d3),同时放弃该 setter 电话。

【讨论】:

谢谢你的好成语。下次我重构这些模式时,我一定会应用它们。对于在您的答案中定义特定类(如 IntermediateResult1)与使用 Reactor 附带的通用 Tuple 类的惯用 Java 有何看法? 没问题。 Java 中的反应式编程仍然是相当新的事物,因此习语和良好实践尚未在所有情况下都得到普遍应用。我怀疑随着时间的推移,这种情况会发生变化,更多这类模式将变得更加普遍。我几乎总是使用命名的中间类而不是通用元组,定义处理的“阶段”并对其进行处理。例如,在目前类似的应用程序中,我将 PreProcessedRecordProcessedRecordAuditedRecord 作为类,并将 preProcessprocessaudit 作为方法名称。 YMMV 关于有效的命名! 如果您无法控制嵌套发布者,您通常会怎么做?像Mono ultVal = Mono.create(callback -&gt; myCallback.apply(someData).flatMap(d -&gt; req.getBodyAsString().flatMap(d1 -&gt; callback.success(resp); return Mono.just(d); )); 这样的东西,我试图将回调本质上链接到事件循环中,并且无法控制“req.getBodyAsString()”。

以上是关于Java Reactor 中嵌套 flatMaps 的好习惯是啥?的主要内容,如果未能解决你的问题,请参考以下文章

像 flatMap() 这样在同一主线程上运行的 Reactor 运算符是不是比常规阻塞代码更有效?

在 reactor Java 中顺序执行 Reactive 任​​务

reactor3 flux 的使用

reactor之操作符

reactor之操作符

flatMap