1. 问题背景
最近在对线索服务进行代码重构。业务场景是一个标准的后台管理查询接口:DCC线索激活列表查询。
这个接口的查询参数比较多,包括:
- 范围查询:
defeatTimeStart/defeatTimeEnd(战败时间范围)
原来的代码使用了大量的 Optional.ofNullable(...).ifPresent(...) 来判空和组装查询条件。虽然逻辑没问题,但满屏的 Optional 看起来确实显得比较臃肿,不够清爽。
2. 优化动机
我想着,MyBatis Plus(MP)原生支持的 Conditional Query(条件构造器) 早就很成熟了。利用它的 condition 参数,可以将原本 3-4 行的 if 逻辑压缩成一行链式调用。
于是,我满怀信心地进行了一波"优雅"的代码重构:
目标 :移除冗余的 if 和 Optional,让代码看起来更整洁、更符合现代 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)
在我的潜意识里,我期望的执行流程是(惰性求值):
- MP 检查
condition(defeatTimeEnd != null)。 - 如果
condition为false,直接短路,不计算后面的参数。 - 如果
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 求值 :
defeatTimeEnd != null-> 结果false。 - 参数 2 求值 :
SacClueInfoDlr::getLastReviewTime-> 结果Function对象。 - 参数 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)时,一定要谨记这个原则:
- 纯变量/引用传递 :放心用链式,代码简洁。
- ✅
.eq(name != null, User::getName, name)
- ✅
- 涉及方法调用/计算 :必须用
if块!- ❌
.eq(obj != null, User::getVal, obj.getValue())------ NPE 预警! - ❌
.in(str != null, User::getIds, str.split(","))------ 逻辑错误/NPE 预警!
- ❌
总结:别为了省那两行 if,让 JVM 教你做人。代码的"优雅"永远建立在"正确"和"安全"之上。