问题复现
假设你在 MyBatis 的 XML 配置中使用了如下代码:
xml
<if test="isCollect != null">
<choose>
<when test="isCollect == 1">
AND exists(select 1 from file_table imgfile2 where task.IMAGE_SEQ=imgfile2.IMAGE_SEQ and imgfile2.DOWNLOAD_STATUS = 1)
</when>
<when test="isCollect == 0">
AND not exists(select 1 from file_table imgfile2 where task.IMAGE_SEQ=imgfile2.IMAGE_SEQ and imgfile2.DOWNLOAD_STATUS = 1)
</when>
</choose>
</if>
在这段代码中,通过 <choose>
标签对 isCollect
的值进行判断。如果 isCollect
的值为 1
,则执行一个 exists
查询;如果 isCollect
的值为 0
,则执行一个 not exists
查询。然而,实际运行时,当 isCollect
为 空字符串 (""
) 时,代码却会意外地执行到 test="isCollect == 0"
这一条件。
具体分析后,得出如果传入的 isCollect
是空字符串 ""
,由于 OGNL 类型转换的原因,空字符串会被转换为 0
,导致条件判断意外地返回 true
,从而执行 SQL 分支。本文将详细解析这个问题的根本原因,并提供有效的解决方案。
1. MyBatis 条件判断的执行流程
在 MyBatis 中,<if test="...">
标签的条件表达式通过 OGNL(Object-Graph Navigation Language)引擎来解析。OGNL 可以动态地访问对象的属性,并执行表达式。MyBatis 将 test
属性中的表达式传递给 OGNL 引擎进行解析和计算,判断条件是否成立。
关键流程:
test
表达式解析 :test="isCollect == 0"
中的isCollect == 0
会被 OGNL 解析并执行。- OGNL 类型转换 :OGNL 在比较值时,会根据目标类型自动进行类型转换。例如,空字符串
""
会被转换为0
(数字),导致条件test="isCollect == 0"
被错误地评估为true
。
2. 源码分析
要理解为什么空字符串 ""
被错误地转换为 0
,我们需要查看 MyBatis 3.5.10 中的关键源码,特别是 OGNL 引擎如何处理这种类型转换。
a. IfSqlNode
类
IfSqlNode
负责解析 <if test="...">
标签中的条件表达式。它会将 test
中的表达式交给 OGNL 引擎进行解析,然后根据条件结果决定是否生成 SQL 片段。
- 源码路径 :
org.apache.ibatis.scripting.xmltags.IfSqlNode
- 主要方法 :
apply(DynamicContext context)
java
# org.apache.ibatis.scripting.xmltags.IfSqlNode#apply
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 如果为true,追加SQL片段
contents.apply(context);
return true;
}
return false;
}
java
# org.apache.ibatis.scripting.xmltags.OgnlCache#getValue
public static Object getValue(String expression, Object root) {
try {
Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
// 获取OGNL表达式的值
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
private static Object parseExpression(String expression) throws OgnlException {
Object node = expressionCache.get(expression);
if (node == null) {
node = Ognl.parseExpression(expression);
expressionCache.put(expression, node);
}
return node;
}
在这个方法中,Ognl.getValue
会评估 test
条件的结果。如果 test
返回 true
,那么相应的 SQL 片段就会被拼接到最终的查询中。
b. OGNL 类型转换:OgnlRuntime.convertValue
OGNL 在执行表达式时会对输入的值进行类型转换,尤其是在数字与字符串比较时。如果传入的是一个空字符串,OGNL 会将其隐式地转换为数字 0
,导致条件判断被误判为 true
。
- 源码路径 :
ognl.OgnlRuntime
- 关键方法 :
convertValue(Object value, Class targetType)
java
public static Object convertValue(Object value, Class targetType) {
if (targetType == int.class || targetType == Integer.class) {
if (value instanceof String) {
return Integer.valueOf((String) value); // 将空字符串转换为 0
}
}
return value;
}
在这段代码中,如果 value
是一个字符串,OGNL 会尝试将其转换为 Integer
。空字符串会被转换为 0
,导致条件 isCollect == 0
被误评估为 true
。
3. 解决方案
为了避免空字符串被错误地转换为 0
,在 test
条件中显式检查 isCollect
是否为 null
或空字符串。
a. 显式检查 null
和空字符串
通过添加显式的判断条件,可以确保 isCollect
既不为 null
也不为空字符串,从而避免 test="isCollect == 0"
误判。改写后的 SQL 语句如下:
xml
<if test="isCollect != null and isCollect != ''">
<choose>
<when test="isCollect == 1">
AND exists(select 1 from file_table imgfile2 where task.IMAGE_SEQ=imgfile2.IMAGE_SEQ and imgfile2.DOWNLOAD_STATUS = 1)
</when>
<when test="isCollect == 0">
AND not exists(select 1 from file_table imgfile2 where task.IMAGE_SEQ=imgfile2.IMAGE_SEQ and imgfile2.DOWNLOAD_STATUS = 1)
</when>
</choose>
</if>
通过这种方式,我们可以确保只有在 isCollect
不为 null
且不为空字符串时,才会进行 test="isCollect == 0"
判断,避免空字符串误判为 0
。
4. 总结
- OGNL 表达式 :在 MyBatis 中,
test="isCollect == 0"
会使用 OGNL 解析,空字符串会被隐式转换为0
,导致条件判断错误。 - 类型转换 :OGNL 会自动将空字符串
""
转换为数字0
,从而导致test="isCollect == 0"
被误判为true
。 - 解决方案 :通过显式检查
isCollect != null && isCollect != ''
来避免空字符串被误判为0
。
参考资料
- MyBatis 官方文档 :MyBatis