Java Stream API:如何在链式处理中记录日志并保持可读性?
这段校验逻辑建议保留 if 和提前返回,方便看清检查顺序、每一步的失败原因和退出位置。也可以使用链式写法,但为了记录日志而引入嵌套 Optional、额外拆包或复杂谓词,通常会让代码更难读。仅为这个需求,没有必要增加第三方依赖。
先分清 Optional 和 Stream
示例使用的是 Optional。它也有 map、filter 和 flatMap,但执行方式与 Stream 不同:
Optional在调用这些方法时就处理已有值,不需要终止操作。- 某一步得到空
Optional后,后续filter的谓词和map的映射函数不会执行。 Optional只表达“有值或无值”,不会保存无值的原因。JDK Optional 文档
这里需要按顺序检查,遇到第一个失败条件时记录日志,并返回 Optional.empty()。
推荐写法:保留提前返回
每个条件都有各自的失败日志时,直接写出检查过程更容易维护:
import java.util.Optional;
public Optional<Address> getValidSupplierAddress(Contact contact) {
if (!contact.hadRecentProjects()) {
log.debug("contact didn't have any recent projects");
return Optional.empty();
}
if (!contact.validatedByBoardOfCommerce()) {
log.debug("contact is not validated by board of commerce");
return Optional.empty();
}
Address address = contact.getAddress();
if (address == null) {
log.debug("no address specified for contact");
return Optional.empty();
}
if (address.isPobox()) {
log.debug("address is not valid, no po. box allowed");
return Optional.empty();
}
return Optional.of(address);
}
这里假设 contact 非 null,且存在 hadRecentProjects() 方法。如果它实际是字段,应使用 contact.hadRecentProjects;Java 方法引用不能直接引用字段。
这种写法没有嵌套分支,新增校验时,在对应位置加入一个检查块即可。排查日志时,也能直接找到触发条件。
如果保留链式结构,控制辅助方法的数量
如果团队习惯使用 Optional 链,可以用一个辅助方法处理判断失败后的动作,再用单独的方法处理获取地址时的空值:
import java.util.Optional;
import java.util.function.Predicate;
public Optional<Address> getValidSupplierAddress(Contact contact) {
return Optional.of(contact)
.filter(onFailure(
Contact::hadRecentProjects,
() -> log.debug(
"contact didn't have any recent projects")))
.filter(onFailure(
Contact::validatedByBoardOfCommerce,
() -> log.debug(
"contact is not validated by board of commerce")))
.flatMap(this::addressWithLogging)
.filter(onFailure(
address -> !address.isPobox(),
() -> log.debug(
"address is not valid, no po. box allowed")));
}
private Optional<Address> addressWithLogging(Contact contact) {
Address address = contact.getAddress();
if (address == null) {
log.debug("no address specified for contact");
}
return Optional.ofNullable(address);
}
private static <T> Predicate<T> onFailure(
Predicate<T> condition,
Runnable action) {
return value -> {
boolean accepted = condition.test(value);
if (!accepted) {
action.run();
}
return accepted;
};
}
onFailure 每次只判断一次条件,并在返回 false 前执行日志动作。在没有异常的情况下,任一检查失败,后续检查都会跳过;所有检查通过时,不会调用失败日志。
原来的 condition.or(...) 也能通过短路求值实现相同行为,不过显式的 if 更容易看出动作的执行时机。这个辅助方法返回的谓词带有日志副作用;如果条件判断抛出异常,失败回调不会自动执行。
原链式代码需要修正什么?
下面的代码不能接收可能为 null 的地址:
Optional.of(contact.getAddress())
Optional.of(null) 会直接抛出 NullPointerException,后面的日志逻辑没有机会运行。允许空值时,应使用 Optional.ofNullable(...)。JDK Optional.of 文档
映射操作则要根据映射函数的返回类型选择:
// getAddress() 返回 Address,允许返回 null
optionalContact.map(Contact::getAddress)
// addressWithLogging() 返回 Optional<Address>
optionalContact.flatMap(this::addressWithLogging)
Optional.map 会把映射结果中的 null 转为空 Optional。如果映射函数已经返回 Optional,使用 flatMap 可以避免得到 Optional<Optional<Address>>。JDK map 与 flatMap 文档
原示例中,Optional.of(supplier.getAddress()) 内部的 filter 接收的是 Address,因此不能在这里使用 Optional::isPresent。修正类型关系后,也不需要再通过 isPresent() 和 get() 手动拆包。
空值日志的位置也需要注意。如果直接写:
optionalContact
.map(Contact::getAddress)
.filter(/* address predicate */);
地址为 null 时,后面的谓词不会执行,因而无法在其中记录“地址缺失”。单独使用 addressWithLogging,就是为了在获取地址时处理这条日志。
真正使用 Stream 时,peek 适合什么?
处理一批联系人时,可以用 peek 观察实际流经某个位置的元素:
contacts.stream()
.filter(Contact::hadRecentProjects)
.peek(contact -> log.debug("contact passed recent-project check"))
.map(Contact::getAddress)
.forEach(this::processAddress);
这里的 processAddress 需要自行定义,并处理可能的空地址。放在 filter 后面的 peek 只能看到通过检查的元素,无法记录被该过滤器排除的元素。
peek 主要用于调试观察。Stream 是惰性执行的,没有终止操作就不会开始遍历。短路操作可能只处理部分元素,某些优化也可能省略阶段。例如,能够直接确定数量的 count() 可能跳过 peek。并行流中的日志调用还可能来自不同线程,日志顺序也可能交错。JDK Stream.peek 文档
必须执行的业务记录或审计记录,应放在明确执行的处理步骤中。这里需要逐项校验并记录失败原因,直接调用校验方法更容易控制行为。
是否值得使用现有库?
只需要返回地址或空值时,JDK 的 Optional 已经够用。失败原因如何表达,仍需要单独设计,换一个链式 API 无法自动解决。
如果调用方也需要知道失败原因,可以考虑返回携带错误信息的结果类型。项目已经使用 Vavr 时,可以用 Either<FailureReason, Address> 表达失败原因或成功地址,按顺序组合检查后,在边界处统一记录日志。如果需要收集多个独立校验错误,Vavr 的 Validation 支持错误累积。Vavr 用户指南
选择时要区分这两种需求:当前代码遇到第一个失败就结束,收集全部错误则会改变执行范围。地址相关的检查仍需以地址存在为前提。仅为减少几个 if 引入一套结果类型,通常不划算。
备注:内容仅供参考。
