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 中。
代码中有各种各样的事情需要解决,但不断出现的事情是嵌套在 flatMap
s 上 Mono
s 用于一系列昂贵的操作,这些操作最终会缩进几个层次,导致无法阅读混乱。我觉得这很烦人,因为我来自 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()
中,您需要引用 d1
、d2
和 d3
,而不仅仅是 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 中的反应式编程仍然是相当新的事物,因此习语和良好实践尚未在所有情况下都得到普遍应用。我怀疑随着时间的推移,这种情况会发生变化,更多这类模式将变得更加普遍。我几乎总是使用命名的中间类而不是通用元组,定义处理的“阶段”并对其进行处理。例如,在目前类似的应用程序中,我将PreProcessedRecord
、ProcessedRecord
和 AuditedRecord
作为类,并将 preProcess
、process
和 audit
作为方法名称。 YMMV 关于有效的命名!
如果您无法控制嵌套发布者,您通常会怎么做?像Mono ultVal = Mono.create(callback -> myCallback.apply(someData).flatMap(d -> req.getBodyAsString().flatMap(d1 -> callback.success(resp); return Mono.just(d); ));
这样的东西,我试图将回调本质上链接到事件循环中,并且无法控制“req.getBodyAsString()”。以上是关于Java Reactor 中嵌套 flatMaps 的好习惯是啥?的主要内容,如果未能解决你的问题,请参考以下文章
像 flatMap() 这样在同一主线程上运行的 Reactor 运算符是不是比常规阻塞代码更有效?