MyBatis 实战中的两个容易踩的坑(不报错但会出错)

作者:明知山 发布时间: 2025-12-25 阅读量:7

在使用 MyBatis 开发过程中,有些问题不会直接抛异常,却会让人花大量时间定位和调试。本文结合两个真实案例,分析其根源及解决方法,希望能帮你在项目中少走弯路。

坑一:Arrays.asList() 与老版本 MyBatis 的反射陷阱

场景回放

某次上线后,测试反馈报了一个奇怪的异常:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'userCode.size() > 0'...

这个异常看起来像是某个 size() 方法调用失败了,但代码里逻辑非常直观:

List<String> userCodes = Arrays.asList("aaa", "bbb", "ccc");
orderService.fetchOrderByUserCodes(userCodes);

SQL Mapper 里也只是简单判断:当 userCode 有值时才拼 IN 条件。

看上去没啥问题,但在老版 MyBatis 下,这段代码却偶现抛了反射访问权限错误。

根本问题分析

  • Arrays.asList() 返回的并不是常见的 java.util.ArrayList
    它内部通过静态内部类 Arrays$ArrayList 实现 List 接口,但这个类是私有的。

  • MyBatis 使用的是 OGNL 表达式引擎来判断 <if test=""> 的条件
    OGNL 在解析表达式时,会通过反射调用 size() 方法。

  • OGNL 的反射逻辑对私有类访问有特殊处理
    因为这个类是私有的,OGNL 默认会调用 setAccessible(true) 来突破访问限制。

  • 老版 MyBatis 在设置 accessible 时没有做好线程同步
    多线程场景下,一个线程把accessible 改成 true,另一个线程可能提前改回了 false,就会导致反射调用失败。这种情况非常偶发。

总结下来,就是:

反射访问私有类的方法 + 并发环境 + 老版本 MyBatis 没有同步处理,才导致这个看似简单的 .size() 调用失败。

解决方案

  1. Arrays.asList() 包装成真实的 ArrayList

    List<String> userCodes = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
    

    这是最直接的修复方式。

  2. 避免在 XML 表达式中调用方法,换成属性判断

    <if test="userCode != null and userCode.length > 0">
    

    这样不需要触发反射方法调用。

  3. 升级 MyBatis 到现代版本

    MyBatis 在 3.3.x 之后修复了这个并发访问的问题,因此升级是更根本的解决方案。

坑二:0 被 OGNL 当成空字符串导致 SQL 过滤条件消失

问题出现

某查询要求筛选状态为 0 的订单。代码和 Mapper 写得很正常:

return orderMapper.queryOrderByStatus(0);

对应 XML 条件是:

<if test="status != null and status != ''">
    AND status = #{status}
</if>

本地测试没问题,但测试环境返回了所有订单(等同于没写 WHERE 条件)!

追查过程

先检查参数确实传成了 0,但拼出来的 SQL 却变成了:

SELECT * FROM t_order WHERE 1=1

也就是说 <if> 条件被判成了 false,SQL 条件被完全丢弃。

源头就是 OGNL 的比较规则

OGNL 在评估表达式 status != '' 时,会自动进行类型转换:

  • status 是数值 0

  • '' 是空字符串

它会先把两边都转成数值:
0 <===> 0.0,结果就是 0 == '' 返回 true,所以 status != '' 判定为 false

正确写法建议

因为数值类型根本不应该跟空字符串比较,应该这样写:

<if test="status != null">
    AND status = #{status}
</if>

如果因为历史原因需要兼顾数值和字符串情况,可以显式写成:

<if test="status != null and (status != '' or status == 0)">
    AND status = #{status}
</if>

核心结论:少犯这些表达式陷阱

1. 避免对基础类型使用空字符串判断

status != '' 这样的写法在 MyBatis/OGNL 下可能被错误解释,特别是数值类型。

2. 对集合类判断长度时避免反射方法

调用 size() 等方法可能触发反射访问,在某些版本里会因为安全 / 并发问题导致失败。

实用建议总结

场景

推荐做法

判断 List 是否有值

不要直接写 .size(),可以先包装成标准 List

判断数值类型参数

只判断 != null

判断字符串参数

判断 != null and != ''

使用 MyBatis 版本

尽可能使用最新版避免老问题

这些坑都不是错代码本身,而是边缘情况下的表达式解析逻辑导致的问题。理解这些细节,可以让 MyBatis 的使用更稳健,避免在高并发或者边界条件下被“无声失败”消磨掉宝贵的排查时间。