11

很开心,在使用 MyBatis 的过程中我踩到一个坑

 4 years ago
source link: https://mp.weixin.qq.com/s/U8Ubl9hj010f937RZbd5zQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

持续输出原创文章,点击蓝字关注我吧

这是why技术的第 14 篇原创文章

在实际开发过程中我踩到了mybatis的一个坑,我觉得值得记录、分享一下。

先说说这个坑是什么吧。如果你踩过这个坑,并且知道具体的原因,那这篇文章可以加深你的印象。如果你没有踩过,那你可得好好看看,因为你总会遇到的。

具体如下: 在mybatis中的OgnlOps.equal(0,"")返回的是true。

faEJneJ.png!web

首先这里返回为true就违背了我们的常识,其次返回为true,会带来什么问题呢?

看完本文你就清楚了。

本文会按照 遇到问题 --> 分析问题 --> 解决问题 的行文思路,用 追踪源码 的方法,对这个问题进行剖析。

同时分享一下我是怎么用 逆向排查 的方法,通过Debug模式找到最关键的那一行源码,然后明白前因后果,最后解决这个问题的。

本文源码: mybatis 3.5.3版本

背景介绍,需求分析

先铺垫一下背景,模拟一个需求。

有一个订单表,表结构如下:

ANjuIfu.png!web

为了简化问题,我们假设表里面只有两条数据:

Mv6FZ3V.png!web

订单号为1234的订单状态为0【关闭

订单号为4321的订单状态为1【开启

已经开发好的功能是模糊查询订单名称,接口如下(图中应该是字符串的OrderName):

MR3mama.png!web

其对应的mapper.xml是这样写的,功能正常:

Jrua2qU.png!web

现在需要在已有功能上添加一个根据状态过滤订单的功能:

yaaeY3I.png!web

假设某个页面有这样的一个下拉框,可以根据订单状态过滤订单数据。

当用户选择【已支付】时,后台接收到的是数字1,用Byte类型接收。

当用户选择【未支付】时,后台接收到的是数字0,用Byte类型接收。

准备开发

现在明确了需求,根据订单状态进行过滤。

很简单,最主要的修改地方就是对mapper.xml的修改,至于怎么从前端传到xml来我就不详细说明了,相信用过mybatis的朋友都知道。

先在接口上加一个入参orderName:

IJ7vm2A.png!web

然后改造一下对应的xml:

UNJvErV.png!web

改造点很简单,在xml文件里面ctrl+c一下原来的if标签,再ctrl+v出来改改里面的名字就好了。

开始自测,遇到问题

请做好单元测试,即使这个功能非常简单,显而易见,你信心十足,但是做好单元测试,是一个程序员应有的职业素养。

单元测试如下:分别传入状态0和1

bIV3mi6.png!web

按照我们现在表里的数据,我们预期的结果是各自查询出一条数据。

Mv6FZ3V.png!web

运行起来,我们一起看看执行结果:

R3Ufey7.png!web

status=0,查询出来的条数 = 2

status=1,查询出来的条数 = 1

aQ7JJrb.jpg!web

这结果和我们预期的不符呀!什么情况?

当时我遇到这个问题的时候,我就知道事情不简单,其中必有蹊跷。

如果是两年前,我遇到问题肯定是立马面向搜索引擎编程。把遇到的问题一顿搜索,根据网友的建议,很快就很解决了。然而,也很快就忘记了。而且,遇到这个问题的时候,我当时是没有联网的。

不要急着去问搜索引擎。不要慌,要分析,冷静分析之后才有收获。

分析问题

分析的第一步其实很容易想到,我们先把sql打印出来,看看最终执行的sql是什么,就知道为什么返回的结果和预期不符了。

所以我们在application.properties里面加上这行配置:

logging.level.com.xxxx.xxxx.mapper = debug

注:上面的xxxx换成自己的mapper包的路径

UnINnqZ.png!web

加上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

打印出来的日志是这样的(截取了其中一部分):

zauyUry.png!web

org.apache.ibatis.logging.jdbc.BaseJdbcLogger的143行 ,debug方法中打印了日志,这行日志就是我的突破口。

在这个地方,我整个sql都拿到了,如果往回走,就能很快的找到sql是在哪里产生的。

那我在BaseJdbcLogger的143行,打上断点,并运行起来。

通过idea的Debug模式,我们可以得到从程序运行开始,到断点处的 整个调用链路 。(如果下面的图片看不清楚,可以点开查看大图):

aYFFn2z.jpg!web

通过调用链,往后走三步,我们可以看到sql是从boundSql中获取到的:

ieaEj2q.jpg!web

那么boundSql是从哪里来的呢?我们继续往回走。

往回走11步,我们可以看到boundSql的获取过程:

eQ7vaqq.jpg!web

为了方便大家找到源码,我把对应的方法名称放在这里: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

faEJneJ.png!web

为什么在mybatis中数字0和空字符串""比返回的是true呢?源码之下无秘密,继续往下Debug你会找到这个地方:

org.apache.ibatis.ognl.OgnlOps#doubleValue

qiYJfyy.jpg!web

这里返回之后,真正的对比是在这里:

267R7v7.png!web

v1和v2最终都变成了0.0。所以返回了true。

由于OgnlOps.equal(0,"")返回为true,所以整个表达式【 OgnlOps.equal(0,"") ? Boolean.FALSE : Boolean.TRUE 返回的是FALSE。

接下来,需要回答的就是这三个问题了:

v1=0是哪里来的?

v2=""是从哪里来的?

返回FALSE会带来什么问题?

AJJ7rui.jpg!web

图中标号为一的地方,就是v1的值,这个0是我传入的查询条件。

图中标号为二的地方,就是v2的值,这个""的来源是我写在mapper.xml文件中if标签里面的表达式。

图中标号为三的地方,为false的原因就是这个表达式 【OgnlOps.equal(0,"") ?Boolean.FALSE : Boolean.TRUE】返回的是false 。返回为false了,就不会进入下面的代码: contents.apply(context)

而这行代码,就是回答我们之前提出问题的后半部分,mybatis通过什么逻辑拼接sql?

326ry2i.jpg!web

就是解析我们写在mapper.xml中的if标签中的test条件,如果满足条件,返回为true则拼接条件里面的内容,即sql。

由于这里的if标签是这样的:

<if test="orderStatus != null and orderStatus != ''">

其中orderStatus!=null返回为true,orderStatus !=''返回为false,所以整个表达式返回为false,则不拼接这个if标签里面的sql。

至此,我们结合源码,对于为什么会出现问题分析完毕。

解决问题

其实问题分析完了,一种解决方法也就呼之欲出,我们只需要把mapper.xml文件中的if标签修改为这样即可:

zYruyu2.png!web

或者改成这样:

rmMnEjv.png!web

再看看执行结果:

EzyiQfY.png!web

这样就和我们预期的结果一致了。

但是,你再回过头的想一想,我最开始的改造mapper.xml是怎么操作的:

改造点很简单,在xml文件里面ctrl+c一下原来的if标签,再ctrl+v出来改改里面的名字就好了。

是的,我无脑的使用了CV大法。导致我在欢声笑语中写出了bug。我orderStatus传入的类型是一个Byte,和""做判断有任何意义吗?

但是我也感谢这次无脑的CV,让我踩到了这个坑,并且研究清楚了。get到了新的知识点。

同时,我也感谢自己做了单元测试,不然测试同学测试的时候抛出这样的问题,我会觉得他不会用,他会觉得我是弱鸡。

最后说几句

在解决这个问题之后,我还是在网上查了一圈,发现也有人遇到了这样的问题,但是我点开搜索出来的第一篇就是一个错误的描述,他说在mybatis中会把0当做null来处理?哥们你看源码了吗?或者说我们说的不是一回事?

J7F7jam.jpg!web

然后还有其他的大量文章都只是扔给你一个解决方法,并没有写为什么这样写就可以解决这个问题。而这样的搜索结果在我看来是不完美的,因为很难留下深刻的印象,导致你或者你同事再次碰见这个问题的时候你会说:哦,这个问题呀,我之前碰见过。怎么解决的,我给忘了。

你这不废话吗?

我之前在《 面试了15位来自211/985院校的2020届研究生之后的思考 》这篇文章中写到一段话,用在这里也很合适:

eUrEFjF.jpg!web

后来我把这个问题分享在群里之后,群里一个朋友也给我分享了一篇文章,肥朝大佬写的 《还有这种操作?浅析为什么要看源码》 。文中给出了另一种解决方案,有理有据,简明扼要,是一篇很好的文章,大家可以看看。

INjyUrJ.jpg!web

尾声

文章写到这里也就接近尾声了。如果你能在这篇文章中get到这个知识点,或者当你碰到这个问题的时候能想起这篇文章,这就是对这篇文章最大的赞赏,文章价值的最高体现。

我更加希望的是,当你碰到这个问题,自己分析完了,在网上查询的时候看到了我的这篇文章。因为自己分析出来的,永远是印象最深刻的,其他的文章只是起点缀作用。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

如果你觉得文章还不错,你的点赞、留言、转发、分享、赞赏就是对我最大的鼓励。

感谢您的阅读,感谢您的关注。

以上。

aqqiyir.jpg!web

持续输出

长按识别关注

往期精彩

原创不易,点个赞吧

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK