大数据 java 代码示例
这是称为“ Functional Java by Example”的系列文章的第7部分。
我在本系列的每个部分中开发的示例是某种“提要处理程序”,用于处理文档。 之前我们已经处理过特殊情况,但是我们将在功能上将它们作为数据来处理,更多。
如果您是第一次来,最好是从头开始阅读。 它有助于了解我们从何处开始以及如何在整个系列中继续前进。
这些都是这些部分:
- 第1部分–从命令式到声明式
- 第2部分–讲故事
- 第3部分–不要使用异常来控制流程
- 第4部分–首选不变性
- 第5部分–将I / O移到外部
- 第6部分–用作参数
- 第7部分–将失败也视为数据
- 第8部分–更多纯函数
我将在每篇文章发表时更新链接。 如果您通过内容联合组织来阅读本文,请查看我博客上的原始文章。
每次代码也被推送到这个GitHub项目 。
优雅失败:回顾不多
这是我们以前留下的东西:
class FeedHandler {
List handle(List changes,
Function creator) {
changes
.findAll { doc -> isImportant(doc) }
.collect { doc ->
creator.apply(doc)
.thenApply { resource ->
setToProcessed(doc, resource)
}
.exceptionally { e ->
setToFailed(doc, e)
}
.get()
}
}
private static boolean isImportant(doc) {
doc.type == 'important'
}
private static Doc setToProcessed(doc, resource) {
doc.copyWith(
status: 'processed',
apiId: resource.id
)
}
private static Doc setToFailed(doc, e) {
doc.copyWith(
status: 'failed',
error: e.message
)
}
}
提要处理程序的主要职责是“处理”已更改文档的列表,这似乎是每次在文档中创建“资源”并进一步处理时。
在上一部分中,该函数已抽象为一个函数,该函数接受Doc
并返回Resource
,在Java中如下所示: Function creator
您可以看到资源实际上包装在CompletableFuture
(CF)中,这使我们可以链接方法调用,例如thenApply
和exceptionally
。 在第3部分(不要使用异常来控制流程) ,我们推出了exceptionally
更换,我们使用了部分try-catch
来应对可能的例外创建资源时。
当时的代码如下:
try {
def resource = createResource(doc)
updateToProcessed(doc, resource)
} catch (e) {
updateToFailed(doc, e)
}
我们将其替换为:
createResource(doc)
.thenAccept { resource ->
updateToProcessed(doc, resource)
}.exceptionally { e ->
updateToFailed(doc, e)
}
CF使我们能够发出“异常”完成的信号,而无需使用诸如抛出Exception
类的副作用。 在Java SDK中,这是封装结果(成功或失败)并与(例如) Optional
(当前值或空值)共享monadic属性的少数类之一。
在其他语言(例如Scala)中,有一种专用类型称为Try
。
尝试
从Scala尝试文档:
Try类型表示可能导致异常或返回成功计算值的计算。
使用Try
Scala开发人员无需在可能发生异常的任何地方进行显式异常处理。 如果我们也要在Java中使用它呢?
幸运的是,有一个名为Vavr的库,其中包含我们可以在Java项目中使用的大量功能实用程序。
Vavr Try文档中的示例向我们展示了完全忘记异常是多么容易:
Try.of(() -> bunchOfWork()).getOrElse(other);
我们要么在成功时从bunchOfWork()
获得结果, bunchOfWork()
在成功过程中因other
而失败。
此类实际上是一个接口,并且有一堆默认方法,它们都返回实例本身,从而可以无限地链接广告 ,例如:
-
andFinally
–提供最终尝试行为,无论操作结果如何。 -
andThen
–如果成功,则运行给定的runnable,否则返回此Failure。 -
filter
–如果是失败或成功,并且值满足谓词,则返回此值。 -
onFailure
–如果这是一次失败,则消耗该throwable。 -
onSuccess
–如果成功,则使用该值。 -
map
–如果给定的函数成功,则运行给定的检查函数,并将当前表达式的结果传递给它。
返回最终值的方法:
-
get
–如果成功则获取此Try的结果,如果失败则获取throw。 -
getCause
–如果失败则获取原因,如果成功则抛出。 -
getOrElse
–返回基础值(如果存在),否则返回另一个值。 -
getOrElseGet
–返回基础值(如果存在),否则返回另一个函数的值。 -
getOrElseThrow
–返回基础值(如果存在),否则抛出getOrElseThrow
()。 -
getOrElseTry
–返回基础值(如果存在),否则返回Try.of(supplier).get()的结果。 -
getOrNull
–返回基础值(如果存在),否则返回null
。
将库包含在项目中后,我们的代码如何受益?
只需将我们的CompletableFuture
替换为Try
。
因此,将我们的调用替换为thenApply/exceptionally
到map/getOrElseGet
'
creator.apply(doc)
.thenApply { resource ->
// ...
}.exceptionally { e ->
// ...
}.get()
变成
creator.apply(doc)
.map { resource ->
// ...
}.getOrElseGet { e ->
// ...
}
Try的map
-method接受一个函数,该函数在try为“成功”时运行(如前所述)。 getOrElseGet
方法可以在发生故障(例如异常)的情况下接受函数。
您可以像在Stream
那样窥视内部,例如
creator.apply(doc)
.peek { resource ->
println "We've got a $resource"
}
.map { resource ->
// ...
}.getOrElseGet { e ->
// ...
}
或者,您可以添加更多日志记录以进行开发或故障排除,例如
creator.apply(doc)
.peek { resource ->
println "We've got a $resource"
}.onSuccess { resource ->
println "Successfully created $resource"
}.onFailure { e ->
println "Bugger! Got a $e"
}.map { resource ->
// ...
}.onSuccess { document ->
println "Successfully processed $document"
}.onFailure { e ->
println "Bugger! Processing failed with $e"
}.getOrElseGet { e ->
// ...
}
从表面上看,似乎没有太大变化。 它只是将一组方法调用替换为其他方法调用,在这种情况下,也就足够了。
但是,您可以选择“ Try
CompletableFuture
因为它似乎更自然地适合我们要实现的目标-计算没有“未来主义”,也没有计划或“在某个时间点”可用。
但是还有更多。
从故障中恢复
现在我们得到的是,如果资源创建者API失败,则将任何失败很好地包装在Try
,因此我们可以轻松地遵循成功或失败的路径。
但是,如果某些故障对我们有意义 ,并且在某些情况下我们希望以其他方式失败的情况仍然成功,该怎么办?
好吧,我们可以从失败中恢复过来 ,并按照自己的意愿修改代码。 我们可以使用下面的Try
方法,并使用漂亮的方法签名,称为recover(Class exception, Function f)
。
它的Javadoc读为:
如果是成功或失败,并且原因不能从cause.getClass()分配,则返回此值。 否则,尝试使用f恢复失败的异常,即调用Try.of(()-> f.apply((X)getCause())。
换句话说:对于特定类型的异常,我们可以提供将失败重新变为成功的功能。
首先,再次删除多余的日志记录和onSuccess/onFailure
。 现在,我们有一个Try
,一个成功场景的map
和一个getOrElseGet
,错误场景的map
:
class FeedHandler {
List handle(List changes,
Function creator) {
changes
.findAll { doc -> isImportant(doc) }
.collect { doc ->
creator.apply(doc)
.map { resource ->
setToProcessed(doc, resource)
}.getOrElseGet { e ->
setToFailed(doc, e)
}
}
}
// ...
}
如果“资源创建” API(即creator#apply
调用)抛出例如DuplicateResourceException
信号,该信号表明我们正在创建的资源是重复项 ,那么它已经存在 ,该怎么办?
我们可以使用recover
功能!
List handle(List changes,
Function creator) {
changes
.findAll { doc -> isImportant(doc) }
.collect { doc ->
creator.apply(doc)
.recover { t ->
handleDuplicate(doc)
}.map { resource ->
setToProcessed(doc, resource)
}.getOrElseGet { e ->
setToFailed(doc, e)
}
}
}
private Resource handleDuplicate(Doc alreadyProcessed) {
// find earlier saved, existing resource and return that one
return repository.findById(alreadyProcessed.getApiId())
}
我们可以在自己的端查找一个重复项(因为它已经被处理过一次),所以我们的“ handleDuplicate”方法将返回任何令人满意的流程 (即Resource
),并且继续进行处理,就好像什么都没有发生一样。
当然,这只是一个示例,但是recover
接受任何接受Throwable
并返回Try
函数。
多种故障:模式匹配
- 如果我们实际上需要确保仅在出现
DuplicateResourceException
情况下才处理“重复”情况,而不是像现在这样,仅处理任何异常,该怎么办? - 如果API可能引发我们还需要专门处理的另一种类型的异常该怎么办? 我们如何在处理多个异常类型的“选择”之间进行选择?
这就是使用Vavr的Match API进行模式匹配的地方。 我们可以创建一个Match
的异常对象x
(通过给使用recover
),同时给予静态of
-方法几种情况选择。
recover { x -> Match(x).of(
Case($(instanceOf(DuplicateResourceException.class)), t -> handleDuplicate(doc)),
Case($(instanceOf(SpecialException.class)), t -> handleSpecial(t))
)}
这个$
实际上是Vavr的静态方法,有几种重载的版本返回一个模式 。
这里的版本是接受Predicate
的所谓“保护模式”。 请从Vavr Javadocs(使用纯Java)中查看另一个示例:
String evenOrOdd(int num) {
return Match(num).of(
Case($(i -> i % 2 == 0), "even"),
Case($(this::isOdd), "odd")
);
}
boolean isOdd(int i) {
return i % 2 == 1;
}
函数的组合( Case
, $
和Match
)在Java中似乎有点奇怪,但是目前还没有本机支持。 同时,您可以将Vavr用于此类功能。
在Java 12中,已经有两个预览功能正在努力使所有这些变为现实。 是JEP 305:instanceof的模式匹配和JEP 325:开关表达式
在本期文章中,我们已经看到我们可以将故障用作数据,例如,走一条替代路径并返回到功能流程。
作为参考,代码现在看起来如下:
class FeedHandler {
List<Doc> handle(List<Doc> changes,
Function<Doc, Try<Resource>> creator) {
changes
.findAll { doc -> isImportant(doc) }
.collect { doc ->
creator.apply(doc)
.recover { x -> Match(x).of(
Case($(instanceOf(DuplicateResourceException.class)), t -> handleDuplicate(doc)),
Case($(instanceOf(SpecialException.class)), t -> handleSpecial(t))
)}
.map { resource ->
setToProcessed(doc, resource)
}.getOrElseGet { e ->
setToFailed(doc, e)
}
}
}
private Resource handleDuplicate(Doc alreadyProcessed) {
// find earlier saved, existing resource and return that one
return repository.findById(alreadyProcessed.getApiId())
}
private Resource handleSpecial(SpecialException e) {
// handle special situation
return new Resource()
}
private static boolean isImportant(doc) {
doc.type == 'important'
}
private static Doc setToProcessed(doc, resource) {
doc.copyWith(
status: 'processed',
apiId: resource.id
)
}
private static Doc setToFailed(doc, e) {
doc.copyWith(
status: 'failed',
error: e.message
)
}
}
由于Groovy 2.x解析器无法正确理解lambda语法,因此上述示例在GitHub上的示例实际上无法正确解析为Groovy,但当然您也可以找到等效的Java版本 。
继续,自己Try
。
下次,我们将以更多功能结束本系列!
如果您有任何意见或建议,我很想听听他们的意见!
翻译自: https://www.javacodegeeks.com/2019/05/functional-java-by-example-treat-failures-data-too.html
大数据 java 代码示例