以为是 MyBatis Plus 链式调用的“优雅”写法,结果反手给我报了个 NPE?

1. 问题背景

最近在对线索服务进行代码重构。业务场景是一个标准的后台管理查询接口:DCC线索激活列表查询

这个接口的查询参数比较多,包括:

  • 范围查询:defeatTimeStart / defeatTimeEnd (战败时间范围)

原来的代码使用了大量的 Optional.ofNullable(...).ifPresent(...) 来判空和组装查询条件。虽然逻辑没问题,但满屏的 Optional 看起来确实显得比较臃肿,不够清爽。

2. 优化动机

我想着,MyBatis Plus(MP)原生支持的 Conditional Query(条件构造器) 早就很成熟了。利用它的 condition 参数,可以将原本 3-4 行的 if 逻辑压缩成一行链式调用。

于是,我满怀信心地进行了一波"优雅"的代码重构:

目标 :移除冗余的 ifOptional,让代码看起来更整洁、更符合现代 Java 风格。

重构前(伪代码):

java 复制代码
Optional.ofNullable(this.defeatTimeEnd)
                .ifPresent(time -> queryWrapper.lt(SacClueInfoDlr::getLastReviewTime, time.plusDays(1)));

重构后(我以为的优雅写法):

java 复制代码
// 👇 看着是不是很爽?一行搞定!
queryWrapper.lt(this.defeatTimeEnd != null, SacClueInfoDlr::getLastReviewTime, this.defeatTimeEnd.plusDays(1));

看着这整齐划一的链式调用,我还在沾沾自喜:这代码重构得,赏心悦目!

3. 踩坑现场

代码上手一测,直接一个 NPE (NullPointerException)

csharp 复制代码
java.lang.NullPointerException:null

我当时第一反应是懵的: "不对啊!我在第一个参数里明明加了 this.defeatTimeEnd != null 的判断啊!MyBatis Plus 不是应该根据这个布尔值来决定是否执行后面的逻辑吗?"

难道 MP 的 condition 机制失效了?

4. 原理深度复盘:JVM 的求值陷阱

冷静下来 Debug 了一下,我发现自己错怪了 MyBatis Plus,我发现自己犯了一个低级但经典的 Java 基础错误:严重混淆了「SQL 的条件构建」和「Java 的参数求值」机制!

让我们重新审视这行代码:

sql 复制代码
java
.lt(condition, column, value)

在我的潜意识里,我期望的执行流程是(惰性求值):

  1. MP 检查 condition (defeatTimeEnd != null)。
  2. 如果 conditionfalse,直接短路,不计算后面的参数
  3. 如果 true,才去计算 value

但在 JVM 眼里,真实世界的 Java 是这样执行的(及早求值 / Eager Evaluation):

在调用 .lt() 方法之前,Java 必须先把所有参数的值计算出来,然后才能入栈调用方法。

为了讲清楚这个问题,我们写一个最简单的原生 Java 方法来模拟:

java 复制代码
// 模拟一个简单的条件执行方法
public void test(boolean condition, String value) {
    // 即使这里有判断,也拦不住外面的 NPE
    if (condition) {
        System.out.println(value);
    }
}

// 调用场景:
String s = null;
// ❌ 这里直接崩了!根本进不到 test 方法里!
test(s != null, s.toString()); 

在调用任何方法(包括 MP 的 .lt())之前,Java 必须先把所有参数的值计算出来,然后才能入栈调用方法。

对于我的那行报错代码:

java 复制代码
.lt(defeatTimeEnd != null, ..., defeatTimeEnd.plusDays(1))
  1. 参数 1 求值defeatTimeEnd != null -> 结果 false
  2. 参数 2 求值SacClueInfoDlr::getLastReviewTime -> 结果 Function 对象。
  3. 参数 3 求值defeatTimeEnd.plusDays(1) -> 💥 炸了!

真相大白 : 因为 defeatTimeEnd 本身是 null,但在传入参数时,我强行调用了它的 .plusDays() 方法。不管你的 condition 是 true 还是 false,这个 .plusDays() 方法在 JVM 准备参数阶段就已经被执行了! 这时候连 MyBatis Plus 的代码大门都还没进去呢。

5. 最终方案:回归朴实

既然明白了原理,那些看似优雅的链式调用在涉及到方法调用(Method Call)复杂计算时,不仅不优雅,反而是个巨大的隐患。

为了系统的稳健性,我果断回滚了这部分逻辑,回归到了最朴实无华的 if 判断:

java 复制代码
// 只有当对象非空时,才进入代码块执行方法调用
if (this.defeatTimeEnd != null) {
    queryWrapper.lt(SacClueInfoDlr::getLastReviewTime, this.defeatTimeEnd.plusDays(1));
}

if (StringUtils.isNotBlank(this.dlrCodeIn)) {
    queryWrapper.in(SacClueInfoDlr::getDlrCode, Arrays.asList(this.dlrCodeIn.split(",")));
}

6. 避坑指南 (Takeaway)

兄弟们,下次在使用 MyBatis Plus 的链式调用(Conditional Query)时,一定要谨记这个原则:

  1. 纯变量/引用传递 :放心用链式,代码简洁。
    • .eq(name != null, User::getName, name)
  2. 涉及方法调用/计算必须用 if 块!
    • .eq(obj != null, User::getVal, obj.getValue()) ------ NPE 预警!
    • .in(str != null, User::getIds, str.split(",")) ------ 逻辑错误/NPE 预警!

总结:别为了省那两行 if,让 JVM 教你做人。代码的"优雅"永远建立在"正确"和"安全"之上。

相关推荐
w***4241 小时前
SpringSecurity的配置
android·前端·后端
皖南大花猪1 小时前
Go 项目中使用 Casbin 实现 RBAC 权限管理完整教程
开发语言·后端·golang·rbac·casbin
源代码•宸1 小时前
GoLang写一个火星漫游行动
开发语言·经验分享·后端·golang
i***13241 小时前
【SpringBoot】单元测试实战演示及心得分享
spring boot·后端·单元测试
s***35301 小时前
SpringMVC新版本踩坑[已解决]
android·前端·后端
D***44142 小时前
【SpringBoot】Spring Boot 项目的打包配置
java·spring boot·后端
5***E6852 小时前
Spring Boot接收参数的19种方式
java·spring boot·后端
u***B7922 小时前
Spring Boot的项目结构
java·spring boot·后端
databook2 小时前
Manim v0.19.1 发布啦!三大新特性让动画制作更丝滑
后端·python·动效