这是 why 技术的第 14 篇原创文章
在实际开发过程中我踩到了 mybatis 的一个坑,我觉得值得记录、分享一下。
先说说这个坑是什么吧。如果你踩过这个坑,并且知道具体的原因,那这篇文章可以加深你的印象。如果你没有踩过,那你可得好好看看,因为你总会遇到的。
具体如下:在 mybatis 中的 OgnlOps.equal(0,””) 返回的是 true。
首先这里返回为 true 就违背了我们的常识,其次返回为 true,会带来什么问题呢?
看完本文你就清楚了。
本文会按照遇到问题 –> 分析问题 –> 解决问题的行文思路,用追踪源码的方法,对这个问题进行剖析。
同时分享一下我是怎么用逆向排查的方法,通过 Debug 模式找到最关键的那一行源码,然后明白前因后果,最后解决这个问题的。
本文源码:mybatis 3.5.3 版本。
背景介绍,需求分析
先铺垫一下背景,模拟一个需求。
有一个订单表,表结构如下:
为了简化问题,我们假设表里面只有两条数据:
订单号为 1234 的订单状态为 0【关闭】
订单号为 4321 的订单状态为 1【开启】
已经开发好的功能是模糊查询订单名称,接口如下:
其对应的 mapper.xml 是这样写的,功能正常:
现在需要在已有功能上添加一个根据状态过滤订单的功能:
假设某个页面有这样的一个下拉框,可以根据订单状态过滤订单数据。
当用户选择【已支付】时,后台接收到的是数字 1,用 Byte 类型接收。
当用户选择【未支付】时,后台接收到的是数字 0,用 Byte 类型接收。
准备开发
现在明确了需求,根据订单状态进行过滤。
很简单,最主要的修改地方就是对 mapper.xml 的修改,至于怎么从前端传到 xml 来我就不详细说明了,相信用过 mybatis 的朋友都知道。
先在接口上加一个入参 orderName:
然后改造一下对应的 xml:
改造点很简单,在 xml 文件里面 ctrl+ c 一下原来的 if 标签,再 ctrl+ v 出来改改里面的名字就好了。
开始自测,遇到问题
请做好单元测试,即使这个功能非常简单,显而易见,你信心十足,但是做好单元测试,是一个程序员应有的职业素养。
单元测试如下:分别传入状态 0 和 1
按照我们现在表里的数据,我们预期的结果是各自查询出一条数据。
运行起来,我们一起看看执行结果:
status=0, 查询出来的条数 = 2
status=1, 查询出来的条数 = 1
这结果和我们预期的不符呀!什么情况?
当时我遇到这个问题的时候,我就知道事情不简单,其中必有蹊跷。
如果是两年前,我遇到问题肯定是立马面向搜索引擎编程。把遇到的问题一顿搜索,根据网友的建议,很快就很解决了。然而,也很快就忘记了。而且,遇到这个问题的时候,我当时是没有联网的。
不要急着去问搜索引擎。不要慌,要分析,冷静分析之后才有收获。
分析问题
分析的第一步其实很容易想到,我们先把 sql 打印出来,看看最终执行的 sql 是什么,就知道为什么返回的结果和预期不符了。
所以我们在 application.properties 里面加上这行配置:
logging.level.com.xxxx.xxxx.mapper = debug
注: 上面的 xxxx 换成自己的 mapper 包的路径
加上 sql 打印后,我们发现当 status 为 0 时,mybatis 并没有给我们拼接 where 关键字。
到这里很自然的就能联想到下一步:为什么 mybatis 没有给我们拼接 where 关键字?
或者换一个问法:mybatis 是在哪里通过上什么逻辑拼接 sql 的?
常规的方法是加断点进行追踪,但是我想分享一个我当时排查的 ” 骚 ” 操作,定位问题非常快。那就是逆向排查。
逆向排查法
现在我们确定了是 sql 拼接的问题,我通过日志,也拿到了完整的 sql。
日志配置是这样的:
logging.pattern.console=%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%C{56}.%method:%L -%msg%n
打印出来的日志是这样的(截取了其中一部分):
在 org.apache.ibatis.logging.jdbc.BaseJdbcLogger 的 143 行,debug 方法中打印了日志,这行日志就是我的突破口。
在这个地方,我整个 sql 都拿到了,如果往回走,就能很快的找到 sql 是在哪里产生的。
那我在 BaseJdbcLogger 的 143 行,打上断点,并运行起来。
通过 idea 的 Debug 模式,我们可以得到从程序运行开始,到断点处的整个调用链路。(如果下面的图片看不清楚,可以点开查看大图):
通过调用链,往后走三步,我们可以看到 sql 是从 boundSql 中获取到的:
那么 boundSql 是从哪里来的呢?我们继续往回走。
往回走 11 步,我们可以看到 boundSql 的获取过程:
为了方便大家找到源码,我把对应的方法名称放在这里:org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
还记得我们开始的问题吗,我们不知道 sql 是在哪里通过什么逻辑拼接的。
而这就是前一部分的答案呀。sql 就是通过 org.apache.ibatis.executor.CachingExecutor 的 81 行代码产生的:
BoundSql boundSql = ms.getBoundSql(parameterObject);
所以接下来我们只需要在这行代码的前面打上断点,我们就能知道后半部分问题的答案了,通过什么逻辑拼接而成?
如果在你不是十分熟悉 mybatis 的情况下,你通过 Debug 模式正向的找到这行代码,是需要花一点时间的,而我上面说的逆向排查,可以节约一大部分时间。
关键源码
后面就是常规的正向查找的过程了,最终你会定位到这个全文最关键的地方:
org.apache.ibatis.ognl.ASTNotEq#getValueBody
为什么在 mybatis 中数字 0 和空字符串 ”” 比返回的是 true 呢?源码之下无秘密,继续往下 Debug 你会找到这个地方:
org.apache.ibatis.ognl.OgnlOps#doubleValue
这里返回之后,真正的对比是在这里:
v1 和 v2 最终都变成了 0.0。所以返回了 true。
由于 OgnlOps.equal(0,””) 返回为 true,所以整个表达式【OgnlOps.equal(0,””)?Boolean.FALSE : Boolean.TRUE】返回的是 FALSE。
接下来,需要回答的就是这三个问题了:
v1= 0 是哪里来的?
v2=”” 是从哪里来的?
返回 FALSE 会带来什么问题?
图中标号为一的地方,就是 v1 的值,这个 0 是我传入的查询条件。
图中标号为二的地方,就是 v2 的值,这个 ”” 的来源是我写在 mapper.xml 文件中 if 标签里面的表达式。
图中标号为三的地方,为 false 的原因就是这个表达式【OgnlOps.equal(0,””)?Boolean.FALSE : Boolean.TRUE】返回的是 false。返回为 false 了,就不会进入下面的代码:contents.apply(context)。
而这行代码,就是回答我们之前提出问题的后半部分,mybatis 通过什么逻辑拼接 sql?
就是解析我们写在 mapper.xml 中的 if 标签中的 test 条件,如果满足条件,返回为 true 则拼接条件里面的内容,即 sql。
由于这里的 if 标签是这样的:
<if test=”orderStatus != null and orderStatus != ””>
其中 orderStatus!=null 返回为 true,orderStatus !=” 返回为 false,所以整个表达式返回为 false,则不拼接这个 if 标签里面的 sql。
至此,我们结合源码,对于为什么会出现问题分析完毕。
解决问题
其实问题分析完了,一种解决方法也就呼之欲出,我们只需要把 mapper.xml 文件中的 if 标签修改为这样即可:
或者改成这样:
再看看执行结果:
这样就和我们预期的结果一致了。
但是,你再回过头的想一想,我最开始的改造 mapper.xml 是怎么操作的:
改造点很简单,在 xml 文件里面 ctrl+ c 一下原来的 if 标签,再 ctrl+ v 出来改改里面的名字就好了。
是的,我无脑的使用了 CV 大法。导致我在欢声笑语中写出了 bug。我 orderStatus 传入的类型是一个 Byte,和 ”” 做判断有任何意义吗?
但是我也感谢这次无脑的 CV,让我踩到了这个坑,并且研究清楚了。get 到了新的知识点。
同时,我也感谢自己做了单元测试,不然测试同学测试的时候抛出这样的问题,我会觉得他不会用,他会觉得我是弱鸡。
最后说几句
在解决这个问题之后,我还是在网上查了一圈,发现也有人遇到了这样的问题,但是我点开搜索出来的第一篇就是一个错误的描述,他说在 mybatis 中会把 0 当做 null 来处理?哥们你看源码了吗?或者说我们说的不是一回事?
然后还有其他的大量文章都只是扔给你一个解决方法,并没有写为什么这样写就可以解决这个问题。而这样的搜索结果在我看来是不完美的,因为很难留下深刻的印象,导致你或者你同事再次碰见这个问题的时候你会说:哦,这个问题呀,我之前碰见过。怎么解决的,我给忘了。
你这不废话吗?
我之前在《面试了 15 位来自 211/985 院校的 2020 届研究生之后的思考》这篇文章中写到一段话,用在这里也很合适:
后来我把这个问题分享在群里之后,群里一个朋友也给我分享了一篇文章,肥朝大佬写的《还有这种操作? 浅析为什么要看源码》。文中给出了另一种解决方案,有理有据,简明扼要,是一篇很好的文章,大家可以看看。
尾声
文章写到这里也就接近尾声了。如果你能在这篇文章中 get 到这个知识点,或者当你碰到这个问题的时候能想起这篇文章,这就是对这篇文章最大的赞赏,文章价值的最高体现。
我更加希望的是,当你碰到这个问题,自己分析完了,在网上查询的时候看到了我的这篇文章。因为自己分析出来的,永远是印象最深刻的,其他的文章只是起点缀作用。
才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
如果你觉得文章还不错,你的点赞、留言、转发、分享、赞赏就是对我最大的鼓励。
感谢您的阅读,感谢您的关注。
以上。
持续输出原创文章,更多精彩原创文章,可以关注公众号后查看哦。
欢迎关注公众号【why 技术】。在这里我会分享一些技术相关的东西,主攻 java 方向,用匠心敲代码,对每一行代码负责。偶尔也会荒腔走板的聊一聊生活,写一写书评,影评。愿你我共同进步。