1. MyBatis开发中的两个深坑解析
作为一名常年与MyBatis打交道的Java开发者,我遇到过不少让人抓狂的Bug。今天要分享的两个案例特别典型——它们都具备"代码看起来完全正确,但运行时就是报错"的特征。第一个坑与Arrays.asList()方法有关,第二个则是MyBatis中数值0的神秘消失问题。通过这两个案例,我想带大家深入理解MyBatis底层的工作原理,以及如何避免这些陷阱。
2. 坑位一:Arrays.asList()遇上老版本MyBatis
2.1 问题现象与背景
那是一个周五的下午(似乎重大Bug总喜欢在周末前出现),测试环境突然抛出异常:
code复制org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'userCode.size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [aaa, bbb] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$UnmodifiableCollection with modifiers "public"]
表面上看,这只是一个普通的size()方法调用失败,但问题远没有这么简单。出问题的代码片段如下:
java复制// Controller层
List<String> userCodes = Arrays.asList("aaa", "bbb", "ccc");
orderService.fetchOrderByUserCodes(userCodes);
对应的Mapper XML:
xml复制SELECT * FROM t_order
WHERE 1=1
<if test="userCode.size() > 0">
AND user_code IN
<foreach collection="userCode" item="code" open="(" separator="," close=")">
#{code}
</foreach>
</if>
2.2 解决方案对比
经过一番排查,我找到了三种解决方案:
方案1:修改入参类型(最快解决)
java复制List<String> userCodes = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
方案2:修改XML表达式(不改Java代码)
xml复制<if test="userCode != null and !userCode.isEmpty()">
方案3:升级MyBatis版本(治本之策)
将MyBatis从3.2.8升级到3.5.13或更高版本。
2.3 问题根源深度解析
这个问题的根源实际上涉及三个层次:
-
类型差异:
Arrays.asList()返回的是java.util.Arrays$ArrayList,而非标准的java.util.ArrayList。虽然都实现了List接口,但前者是Arrays的私有静态内部类。 -
访问权限问题:MyBatis使用OGNL表达式引擎解析XML中的条件判断。当OGNL尝试通过反射调用
Arrays$ArrayList的size()方法时,由于该类是private static class,需要调用setAccessible(true)来绕过权限检查。 -
并发Bug:MyBatis 3.2.x版本在处理反射时存在并发问题——
accessible标志的设置和恢复不是原子操作,可能导致多线程环境下访问失败。
2.4 经验总结与最佳实践
- 使用
Arrays.asList()时要特别注意其返回的是不可变列表 - 需要可变列表时,使用
new ArrayList<>(Arrays.asList(...))包装 - 保持MyBatis版本更新,避免已知的并发问题
- XML中的集合判空优先使用
!= null and !empty而非直接调用size()
3. 坑位二:参数传0导致SQL条件神秘消失
3.1 问题现象描述
另一个周五下午,我实现了一个简单的查询功能:
java复制public List<Order> queryPendingOrders() {
return orderMapper.queryOrderByStatus(0); // 0表示待支付
}
对应的Mapper XML:
xml复制SELECT * FROM t_order
WHERE 1=1
<if test="status != null and status != ''">
AND status = #{status}
</if>
本地测试正常,但测试环境却返回了所有状态的订单,status=0的条件神秘消失了。
3.2 问题排查过程
查看执行的SQL日志:
code复制SELECT * FROM t_order WHERE 1=1
发现status条件确实被忽略了。问题出在XML的if判断上:
xml复制<if test="status != null and status != ''">
当status=0时,这个表达式被求值为false,导致条件被跳过。
3.3 OGNL类型转换机制解析
这个问题源于OGNL表达式的类型转换特性:
- OGNL会将数值0与空字符串""都转换为double类型的0.0进行比较
- 因此
0 != ''实际上被转换为0.0 != 0.0,结果为false - 这导致if条件不成立,SQL条件被忽略
3.4 解决方案与最佳实践
针对不同场景,推荐以下写法:
场景1:数值类型参数
xml复制<if test="status != null">
场景2:需要兼容数值和字符串
xml复制<if test="status != null and (status != '' or status == 0)">
场景3:字符串类型参数
xml复制<if test="userName != null and userName != ''">
3.5 避坑指南
- 数值类型参数不要使用空字符串判断
- 明确区分数值类型和字符串类型的判空逻辑
- 必要时显式处理0值的情况
- 理解OGNL的类型转换规则,避免隐式转换带来的问题
4. MyBatis开发中的常见问题与排查技巧
4.1 动态SQL中的常见陷阱
-
字符串比较问题:
- 使用
==比较字符串可能不生效 - 推荐使用
.equals()方法或OGNL的字符串比较语法
- 使用
-
布尔值判断:
test="flag"和test="flag == true"行为可能不同- 建议统一使用
test="flag"或test="flag == true"风格
-
集合操作:
- 避免在OGNL表达式中直接new集合对象
- 集合判空优先使用
!empty而非size() > 0
4.2 性能优化建议
-
避免过度使用动态SQL:
- 动态SQL会增加解析开销
- 对于固定条件,尽量使用静态SQL
-
合理使用缓存:
- 理解MyBatis的一级缓存和二级缓存机制
- 对于频繁查询但很少变更的数据,考虑启用缓存
-
批量操作优化:
- 使用
<foreach>批量插入时注意批次大小 - 大批量操作考虑使用BatchExecutor
- 使用
4.3 调试技巧
-
开启MyBatis日志:
properties复制logging.level.org.mybatis=DEBUG -
使用MyBatis插件:
- 开发自定义插件拦截SQL执行
- 使用现有插件如PageHelper、MyBatis-Plus等
-
IDE调试技巧:
- 在
org.apache.ibatis.scripting.xmltags包设置断点 - 跟踪SQL解析和执行过程
- 在
5. 从原理理解MyBatis的工作机制
5.1 OGNL表达式引擎解析
MyBatis使用OGNL(Object-Graph Navigation Language)来解析动态SQL中的表达式。理解OGNL的几个关键特性:
-
类型转换规则:
- 字符串和数值之间的自动转换
- 布尔值的特殊处理
-
方法调用机制:
- 通过反射调用对象方法
- 访问控制权限的处理
-
表达式求值流程:
- 从参数对象中获取值
- 应用运算符和函数
- 返回最终结果
5.2 MyBatis的SQL解析过程
-
XML解析阶段:
- 解析mapper文件为内存对象
- 构建SQL源信息
-
动态SQL处理:
- 解析
<if>,<foreach>等标签 - 应用OGNL表达式求值
- 解析
-
SQL生成阶段:
- 拼接最终SQL语句
- 处理参数占位符
5.3 反射机制的并发问题
本文第一个坑的根本原因是反射API的并发问题:
AccessibleObject.setAccessible()不是线程安全的- MyBatis 3.2.x版本没有正确处理这个并发场景
- 新版本通过同步控制解决了这个问题
理解这些底层机制,可以帮助我们更好地规避类似问题。
6. 实际开发中的经验分享
在长期使用MyBatis的过程中,我总结了以下几点经验:
-
保持版本更新:
- 定期检查MyBatis的版本更新
- 关注官方issue和修复记录
-
编写可维护的动态SQL:
- 保持动态SQL简洁明了
- 复杂的逻辑考虑拆分为多个查询
-
单元测试覆盖:
- 特别测试边界条件(如空集合、0值等)
- 验证动态SQL的各种分支
-
文档和注释:
- 为复杂的动态SQL添加注释
- 记录特殊的OGNL表达式用法
-
团队知识共享:
- 定期分享MyBatis的使用经验
- 建立团队内的最佳实践指南
这些经验看似简单,但在实际项目中能显著提高开发效率和代码质量。