很開心,在使用mybatis的過程中我踩到一個坑

在實際開發過程中我踩到了mybatis的一個坑,我覺得值得記錄、分享一下。

先說說這個坑是什麼吧。如果你踩過這個坑,並且知道具體的原因,那這篇文章可以加深你的印象。如果你沒有踩過,那你可得好好看看,因為你總會遇到的。

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

很開心,在使用mybatis的過程中我踩到一個坑

首先這裡返回為true就違背了我們的常識,其次返回為true,會帶來什麼問題呢?

看完本文你就清楚了。

本文會按照遇到問題 --> 分析問題 --> 解決問題的行文思路,用追蹤源碼的方法,對這個問題進行剖析。

同時分享一下我是怎麼用逆向排查的方法,通過Debug模式找到最關鍵的那一行源碼,然後明白前因後果,最後解決這個問題的。

本文源碼:mybatis 3.5.3版本

背景介紹,需求分析

先鋪墊一下背景,模擬一個需求。

有一個訂單表,表結構如下:

很開心,在使用mybatis的過程中我踩到一個坑

為了簡化問題,我們假設表裡面只有兩條數據:

很開心,在使用mybatis的過程中我踩到一個坑

訂單號為1234的訂單狀態為0【關閉】

訂單號為4321的訂單狀態為1【開啟】

已經開發好的功能是模糊查詢訂單名稱,接口如下:

很開心,在使用mybatis的過程中我踩到一個坑

其對應的mapper.xml是這樣寫的,功能正常:

很開心,在使用mybatis的過程中我踩到一個坑

現在需要在已有功能上添加一個根據狀態過濾訂單的功能:

很開心,在使用mybatis的過程中我踩到一個坑

假設某個頁面有這樣的一個下拉框,可以根據訂單狀態過濾訂單數據。

當用戶選擇【已支付】時,後臺接收到的是數字1,用Byte類型接收。

當用戶選擇【未支付】時,後臺接收到的是數字0,用Byte類型接收。

準備開發

現在明確了需求,根據訂單狀態進行過濾。

很簡單,最主要的修改地方就是對mapper.xml的修改,至於怎麼從前端傳到xml來我就不詳細說明了,相信用過mybatis的朋友都知道。

先在接口上加一個入參orderName:

很開心,在使用mybatis的過程中我踩到一個坑

然後改造一下對應的xml:

很開心,在使用mybatis的過程中我踩到一個坑

改造點很簡單,在xml文件裡面ctrl+c一下原來的if標籤,再ctrl+v出來改改裡面的名字就好了。

開始自測,遇到問題

請做好單元測試,即使這個功能非常簡單,顯而易見,你信心十足,但是做好單元測試,是一個程序員應有的職業素養。

單元測試如下:分別傳入狀態0和1

很開心,在使用mybatis的過程中我踩到一個坑

按照我們現在表裡的數據,我們預期的結果是各自查詢出一條數據。

很開心,在使用mybatis的過程中我踩到一個坑

運行起來,我們一起看看執行結果:

很開心,在使用mybatis的過程中我踩到一個坑

status=0,查詢出來的條數 = 2

status=1,查詢出來的條數 = 1

很開心,在使用mybatis的過程中我踩到一個坑

這結果和我們預期的不符呀!什麼情況?

當時我遇到這個問題的時候,我就知道事情不簡單,其中必有蹊蹺。

如果是兩年前,我遇到問題肯定是立馬面向搜索引擎編程。把遇到的問題一頓搜索,根據網友的建議,很快就很解決了。然而,也很快就忘記了。而且,遇到這個問題的時候,我當時是沒有聯網的。

不要急著去問搜索引擎。不要慌,要分析,冷靜分析之後才有收穫。

分析問題

分析的第一步其實很容易想到,我們先把sql打印出來,看看最終執行的sql是什麼,就知道為什麼返回的結果和預期不符了。

所以我們在application.properties裡面加上這行配置:

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

注:上面的xxxx換成自己的mapper包的路徑

很開心,在使用mybatis的過程中我踩到一個坑

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

打印出來的日誌是這樣的(截取了其中一部分):

很開心,在使用mybatis的過程中我踩到一個坑

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

在這個地方,我整個sql都拿到了,如果往回走,就能很快的找到sql是在哪裡產生的。

那我在BaseJdbcLogger的143行,打上斷點,並運行起來。

通過idea的Debug模式,我們可以得到從程序運行開始,到斷點處的整個調用鏈路。(如果下面的圖片看不清楚,可以點開查看大圖):

很開心,在使用mybatis的過程中我踩到一個坑

通過調用鏈,往後走三步,我們可以看到sql是從boundSql中獲取到的:

很開心,在使用mybatis的過程中我踩到一個坑

那麼boundSql是從哪裡來的呢?我們繼續往回走。

往回走11步,我們可以看到boundSql的獲取過程:

很開心,在使用mybatis的過程中我踩到一個坑

為了方便大家找到源碼,我把對應的方法名稱放在這裡: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的過程中我踩到一個坑

為什麼在mybatis中數字0和空字符串""比返回的是true呢?源碼之下無秘密,繼續往下Debug你會找到這個地方:

org.apache.ibatis.ognl.OgnlOps#doubleValue

很開心,在使用mybatis的過程中我踩到一個坑

這裡返回之後,真正的對比是在這裡:

很開心,在使用mybatis的過程中我踩到一個坑

v1和v2最終都變成了0.0。所以返回了true。

由於OgnlOps.equal(0,"")返回為true,所以整個表達式【OgnlOps.equal(0,"") ?Boolean.FALSE : Boolean.TRUE】返回的是FALSE。

接下來,需要回答的就是這三個問題了:

v1=0是哪裡來的?

v2=""是從哪裡來的?

返回FALSE會帶來什麼問題?

很開心,在使用mybatis的過程中我踩到一個坑

圖中標號為一的地方,就是v1的值,這個0是我傳入的查詢條件。

圖中標號為二的地方,就是v2的值,這個""的來源是我寫在mapper.xml文件中if標籤裡面的表達式。

圖中標號為三的地方,為false的原因就是這個表達式【OgnlOps.equal(0,"") ?Boolean.FALSE : Boolean.TRUE】返回的是false。返回為false了,就不會進入下面的代碼:contents.apply(context)

而這行代碼,就是回答我們之前提出問題的後半部分,mybatis通過什麼邏輯拼接sql?

很開心,在使用mybatis的過程中我踩到一個坑

就是解析我們寫在mapper.xml中的if標籤中的test條件,如果滿足條件,返回為true則拼接條件裡面的內容,即sql。

由於這裡的if標籤是這樣的:

其中orderStatus!=null返回為true,orderStatus !=''返回為false,所以整個表達式返回為false,則不拼接這個if標籤裡面的sql。

至此,我們結合源碼,對於為什麼會出現問題分析完畢。

解決問題

其實問題分析完了,一種解決方法也就呼之欲出,我們只需要把mapper.xml文件中的if標籤修改為這樣即可:

很開心,在使用mybatis的過程中我踩到一個坑

或者改成這樣:

很開心,在使用mybatis的過程中我踩到一個坑

再看看執行結果:

很開心,在使用mybatis的過程中我踩到一個坑

這樣就和我們預期的結果一致了。

但是,你再回過頭的想一想,我最開始的改造mapper.xml是怎麼操作的:

改造點很簡單,在xml文件裡面ctrl+c一下原來的if標籤,再ctrl+v出來改改裡面的名字就好了。

是的,我無腦的使用了CV大法。導致我在歡聲笑語中寫出了bug。我orderStatus傳入的類型是一個Byte,和""做判斷有任何意義嗎?

但是我也感謝這次無腦的CV,讓我踩到了這個坑,並且研究清楚了。get到了新的知識點。

同時,我也感謝自己做了單元測試,不然測試同學測試的時候拋出這樣的問題,我會覺得他不會用,他會覺得我是弱雞。

最後說幾句

在解決這個問題之後,我還是在網上查了一圈,發現也有人遇到了這樣的問題,但是我點開搜索出來的第一篇就是一個錯誤的描述,他說在mybatis中會把0當做null來處理?哥們你看源碼了嗎?或者說我們說的不是一回事?

很開心,在使用mybatis的過程中我踩到一個坑

然後還有其他的大量文章都只是扔給你一個解決方法,並沒有寫為什麼這樣寫就可以解決這個問題。而這樣的搜索結果在我看來是不完美的,因為很難留下深刻的印象,導致你或者你同事再次碰見這個問題的時候你會說:哦,這個問題呀,我之前碰見過。怎麼解決的,我給忘了。

你這不廢話嗎?

我之前在《面試了15位來自211/985院校的2020屆研究生之後的思考》這篇文章中寫到一段話,用在這裡也很合適:

很開心,在使用mybatis的過程中我踩到一個坑

後來我把這個問題分享在群裡之後,群裡一個朋友也給我分享了一篇文章,肥朝大佬寫的《還有這種操作?淺析為什麼要看源碼》。文中給出了另一種解決方案,有理有據,簡明扼要,是一篇很好的文章,大家可以看看。

很開心,在使用mybatis的過程中我踩到一個坑

尾聲

文章寫到這裡也就接近尾聲了。如果你能在這篇文章中get到這個知識點,或者當你碰到這個問題的時候能想起這篇文章,這就是對這篇文章最大的讚賞,文章價值的最高體現。

我更加希望的是,當你碰到這個問題,自己分析完了,在網上查詢的時候看到了我的這篇文章。因為自己分析出來的,永遠是印象最深刻的,其他的文章只是起點綴作用。

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。

以上。


分享到:


相關文章: