适用版本:MyBatis 3.x (动态 SQL 解析依赖 MyBatis 及底层 OGNL 引擎,与 mybatis版本无关)
一、背景
在使用 Spring Boot + MyBatis-Plus 开发时,mapper.xml 中的 <if test="..."> 表达式基于 OGNL(Object-Graph Navigation Language) 解析。由于 OGNL 对字面量和类型的处理方式与 Java 直觉存在差异,容易引发两类典型错误:
-
字符串比较时,用单引号括单字符导致被解析为
char类型,比较失效 -
数值
0在条件判断中表现不符合预期 -
==与equals()的行为差异
以下分别分析原因、给出示例,并附源码依据。
二、字符串比较:单引号括单字符 → char 类型陷阱
2.1 问题现象
XML
<!-- 失效写法:外双引号,内单引号 -->
<if test="appType.equals('P')"> <!-- 永远为 false -->
<!-- 生效写法:外单引号,内双引号 -->
<if test='appType.equals("P")'> <!-- 正常工作 -->
2.2 原因分析
OGNL 解析字面量时:
-
单引号内只有 1 个字符 (如
'P') → 解析为 Java 的char类型 -
双引号内任意内容(如
"P") → 解析为String类型 -
单引号内长度 > 1(如
'Yes') → 解析为String类型
当 appType 为 String 时,调用 appType.equals('P') 相当于 String.equals(char),类型不匹配,OGNL 无法正确求值(通常返回 false 或引发异常)。
2.3 示例对比
| 写法 | OGNL 解析 'P' 类型 |
OGNL 解析 "P" 类型 |
比较结果 |
|---|---|---|---|
test="appType.equals('P')" |
char |
--- | ❌ 失效 |
test='appType.equals("P")' |
--- | String |
✅ 生效 |
test="appType == 'P'" |
char |
--- | ❌ 失效(同上) |
test='appType == "P"' |
--- | String |
✅ 生效 |
✅ 推荐统一使用 外单内双 的引号组合,并直接用
==或equals。
三、数值 0 条件判断出错
3.1 常见错误场景
XML
<!-- 场景1:直接写 0 作为条件 -->
<if test="0"> <!-- 永远为 false,因为 0 转为 false -->
<!-- 场景2:字符串与数字 0 比较 -->
<if test="status == 0"> <!-- status 为 "" 或 "abc" 时会抛异常 -->
3.2 原因分析
OGNL 中的布尔转换规则(来自 OgnlOps.booleanValue()):
-
数值
0→false;任何非0数值(含负数) →true -
null→false -
空字符串
""→false;非空字符串 →true
此外,OGNL 在 == 比较时会尝试类型转换:
-
"0" == 0→ 将字符串"0"转为数字 0 →true -
"" == 0→ 将空串转为数字失败 → 抛出NumberFormatException -
"abc" == 0→ 同样抛出转换异常
3.3 正确做法
XML
<!-- 检查数值是否为 0 -->
<if test="status != null and status == 0">
<!-- 检查标志位(0 表示 false) -->
<if test="flag == 0"> <!-- 不要直接写 test="0" -->
四、== 与 equals() 在 OGNL 中的本质区别
| 特性 | == |
equals() |
|---|---|---|
| 是否隐式类型转换 | ✅ 是(数值、字符串、布尔之间自动转换) | ❌ 否 |
null 安全 |
null == x → false(不抛异常) |
null.equals(x) → NullPointerException |
| 典型行为 | '1' == 1 → true 0 == false → true "true" == true → true |
'1'.equals(1) → false 0.equals(false) → 编译错误或 false |
| 推荐场景 | 大多数动态 SQL 比较(更灵活、安全) | 需要严格类型匹配且保证非 null 时 |
结论:在
<if test>中,优先使用==,并保证两侧类型可安全转换或显式处理null。
五、源码解析(OGNL 3.x)
5.1 OGNL 如何将字面量解析为 char vs String
OGNL 词法分析器 OgnlParser 在解析单引号字符串时,会根据长度决定创建 Character 还是 String 对象。相关逻辑位于 ASTStringLiteral 节点:
java
// 简化逻辑
public class ASTStringLiteral extends SimpleNode {
public Object getValue(OgnlContext context, Object source) {
String literal = image.substring(1, image.length() - 1);
if (literal.length() == 1) {
return literal.charAt(0); // 返回 Character
} else {
return literal; // 返回 String
}
}
}
而双引号字符串 "..." 始终直接返回 String。
5.2 布尔值转换:OgnlOps.booleanValue()
java
public static boolean booleanValue(Object value) {
if (value == null) return false;
if (value instanceof Boolean) return (Boolean) value;
if (value instanceof Number) {
return ((Number) value).doubleValue() != 0.0; // 0 → false
}
if (value instanceof Character) {
return ((Character) value).charValue() != 0;
}
if (value instanceof String) {
return ((String) value).length() > 0; // 空串→false
}
return true;
}
5.3 == 比较核心:OgnlOps.isEqual()
java
public static boolean isEqual(Object v1, Object v2) {
if (v1 == v2) return true;
if (v1 == null || v2 == null) return false;
// 数值与数值
if (v1 instanceof Number && v2 instanceof Number) {
return ((Number) v1).doubleValue() == ((Number) v2).doubleValue();
}
// 数值与字符串互转
if (v1 instanceof Number && v2 instanceof String) {
return isEqual(v1, toNumber((String) v2));
}
if (v1 instanceof String && v2 instanceof Number) {
return isEqual(toNumber((String) v1), v2);
}
// 布尔与数值互转
if (v1 instanceof Boolean && v2 instanceof Number) {
return ((Boolean) v1) == (((Number) v2).doubleValue() != 0.0);
}
if (v1 instanceof Number && v2 instanceof Boolean) {
return (((Number) v1).doubleValue() != 0.0) == ((Boolean) v2);
}
// ... 其他类型转换
return v1.equals(v2);
}
可见 OGNL 的 == 远比 Java 的 == 更"智能",会主动进行跨类型转换。
六、最佳实践总结
| 场景 | 推荐写法 | 说明 |
|---|---|---|
| 字符串比较 | <if test='str == "value"'> |
外单内双,直接用 == |
| 数值比较 | <if test="num == 0"> |
确保 num 是数字或纯数字字符串 |
| 布尔判断 | <if test="flag == 1"> 或 <if test="flag == true"> |
不要直接写 <if test="1"> 或 <if test="0"> |
防止 null 异常 |
<if test="obj != null and obj == 1"> |
先用 != null 再比较 |
| 严格类型匹配 | <if test='"P".equals(appType)'> |
用常量放前面避免 NPE |
| 禁止写法 | ❌ test="status.equals(0)" ❌ test="type == 'A'" |
前者 NPE,后者 char 陷阱 |
七、结语
MyBatis 动态 SQL 中的 OGNL 表达式陷阱根源在于 OGNL 引擎独特的类型推断和隐式转换规则 ,而非 MyBatis / MyBatis-Plus 的缺陷。理解 char vs String 字面量解析、数值 0 的布尔语义、== 与 equals 的差异,即可安全编写动态 SQL。
牢记:单字符用双引号包(
"A"),数值比较显式写,多用==慎用equals。