MyBatis踩坑实录:那些不报错但让你debug到深夜的Bug

📅 2026/6/30 18:52:37
MyBatis踩坑实录:那些不报错但让你debug到深夜的Bug
说实话MyBatis这玩意儿平时挺好用的但有时候报的错真让人摸不着头脑。尤其是那种本地跑得好好的一上线就炸的Bug简直让人怀疑人生。今天就记录两个让我debug到深夜的坑它们都有个共同特点代码看起来完全没问题但运行时就是莫名其妙地报错。如果你也被MyBatis折磨过这篇文章可能会让你会心一笑原来不是我一个人踩过这些坑。坑位一Arrays.asList() 遇上老版本MyBatis3.2.x版本事故现场周五下午四点半是的Bug总是在快下班时出现测试环境突然报了个令人头大的异常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] at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:364) at $Proxy15.selectList(Unknown Source) at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:194) at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:114) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:58) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:43) at $Proxy18.fetchOrder(Unknown Source) at com.xx.xx.server.impl.XX.fetchOrderByUnitNo(RechargeCardBillServiceImpl.java:351) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:198) at $Proxy26.fetchOrderByUnitNo(Unknown Source) at com.ofpay.ofdc.task.AbstractRechargeTask.run(AbstractRechargeTask.java:65) at java.lang.Thread.run(Thread.java:662)看到这个异常我第一反应是什么鬼size()方法还能调用失败来看看出问题的代码// Controller层 ListString userCodes Arrays.asList(aaa, bbb, ccc); orderService.fetchOrderByUserCodes(userCodes);!-- Mapper.xml -- select idfetchOrder resultTypeOrder SELECT * FROM t_order WHERE 11 if testuserCode ! null and userCode.size() 0 AND user_code IN foreach collectionuserCode itemcode open( close) separator, #{code} /foreach /if /select这代码看起来没啥问题啊userCode不为空调个size()方法判断长度天经地义。但它就是报错了而且是偶现一般偶现都有大坑。先说解决方案一顿ChatGPT Google Stack Overflow搜索后找到了三种解决办法方案1改入参类型最快// 把Arrays.asList返回的假ArrayList转成真正的ArrayList ListString userCodes new ArrayList(Arrays.asList(aaa, bbb, ccc));改完重新发布问题秒解决。测试验证通过终于可以下班了。方案2改XML表达式不改Java代码!-- 用length属性替代size()方法 -- if testuserCode ! null and userCode.length 0 AND user_code IN ... /if这个方案也能work而且不用改业务代码改完就能用。方案3升级MyBatis版本治本之策!-- 从老古董版本 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.2.8/version !-- 2014年的版本 -- /dependency !-- 升级到现代版本 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency不过这个方案需要做全面的回归测试周五晚上就算了留到下周慢慢搞。刨根问底这到底是个啥坑线上问题解决了但总感觉哪里不对劲。周末闲着没事决定把这个诡异的异常刨根问底搞清楚。翻了半天资料终于明白是怎么回事了。第一层问题类型不同Arrays.asList()返回的不是我们熟悉的java.util.ArrayList而是java.util.Arrays的一个私有静态内部类Arrays$ArrayList。写个简单的测试验证一下ListString list1 Arrays.asList(a, b, c); ListString list2 new ArrayList(Arrays.asList(a, b, c)); System.out.println(list1.getClass()); // 输出: class java.util.Arrays$ArrayList System.out.println(list2.getClass()); // 输出: class java.util.ArrayList看到没一个是Arrays$ArrayList一个是ArrayList虽然都实现了List接口但类型完全不同。第二层问题访问权限异常MyBatis用OGNL表达式引擎来解析XML中的条件判断比如userCode.size() 0。当OGNL尝试通过反射调用Arrays$ArrayList的size()方法时发现这个类是private static class私有静态内部类。虽然size()方法本身是public的但因为类本身是private修饰符OGNL在反射访问时需要调用setAccessible(true)来绕过权限检查。问题就出在这里第三层问题并发Bug重点来了老版本MyBatis在处理反射时有个并发问题当需要调用私有类的方法时会先设置accessible true调用完再设回false。但这个操作没有加锁非原子性想象一下这个场景线程A设置accessible true准备调用方法线程B也设置accessible true然后调用方法再设回false线程A此时去调用方法发现accessible已经被B改成false了boom这就是为什么这个Bug偶尔才出现因为它本质上是个并发问题只有在高并发场景下多个线程同时调用这个接口时才会触发。GitHub上有人早在2014年就提了这个issuemybatis/mybatis-3#384后来MyBatis在3.3.x版本修复了这个问题对反射操作加了同步控制确保accessible的设置和方法调用是原子操作。坑位二参数传0SQL条件神秘消失之谜又一个周五的故事是的又是周五下午墨菲定律Bug永远在周五出现。需求很简单查询所有待支付状态status0的订单。十分钟写完代码// Service层 public ListOrder queryPendingOrders() { return orderMapper.queryOrderByStatus(0); // 0表示待支付 }!-- Mapper.xml -- select idqueryOrderByStatus resultTypeOrder SELECT * FROM t_order WHERE 11 if teststatus ! null and status ! AND status #{status} /if /select本地测试完美运行。提交代码合并主干发布测试环境。心想这次稳了准备提前收拾东西下班。结果半小时后测试同学发来消息这个接口有问题啊怎么把所有状态的订单都查出来了我要的是status0的订单。我一脸懵逼不可能啊我刚测过的明明没问题打开测试环境日志执行的SQL是SELECT * FROM t_order WHERE 11WHERE后面的status条件呢被吃了Debug之旅我在本地打断点一步步调试Controller层传入的参数status 0✅Service层收到的参数status 0✅MyBatis执行的SQLWHERE 11❌问题肯定出在XML的if判断上。盯着这行看了好几分钟if teststatus ! null and status ! 突然灵光一现会不会是 0 被判定成了空字符串赶紧改成这样试试if teststatus ! null AND status #{status} /if重新发布问题解决测试环境查询status0的订单正常返回了。原理揭秘OGNL的类型转换陷阱这又是一个MyBatis准确说是OGNL的经典坑。这个坑比第一个还隐蔽因为它不会报错而是悄悄地把你的条件吃掉。OGNL的求值逻辑MyBatis的if标签用的是OGNL表达式引擎。当你写status ! 时OGNL内部会经历这样的判断流程先通过OgnlCache.getValue()获取表达式的值这里表达式返回了false然后在ExpressionEvaluator.evaluateBoolean()中判断这个值根据返回的不同类型作不同判断最终返回boolean类型结果。先看第二步OGNL对不同类型有不同的判断逻辑// ExpressionEvaluator.evaluateBoolean()方法 OGNL的判断逻辑 public boolean evaluateBoolean(String expression, Object parameterObject) { // 这里value返回的是false Object value OgnlCache.getValue(expression, parameterObject); if (value instanceof Boolean) { // 因此会走到这里返回false return (Boolean) value; } if (value instanceof Number) { return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) ! 0; } return value ! null; }类型转换的坑当你写status ! 时从OgnlCache.getValue()往下不断追溯OGNL最终会调用compareWithConversion方法做类型转换比较。这个方法会把两边的值都转成同一类型再比较数值0会被转成double类型0.0