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

這是why技術的第14篇原創文章前端

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

先說說這個坑是什麼吧。若是你踩過這個坑,而且知道具體的緣由,那這篇文章能夠加深你的印象。若是你沒有踩過,那你可得好好看看,由於你總會遇到的。程序員

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

file

首先這裏返回爲true就違背了咱們的常識,其次返回爲true,會帶來什麼問題呢?sql

看完本文你就清楚了。apache

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

同時分享一下我是怎麼用逆向排查的方法,經過Debug模式找到最關鍵的那一行源碼,而後明白來龍去脈,最後解決這個問題的。segmentfault

本文源碼:mybatis 3.5.3版本。session

背景介紹,需求分析

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

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

file

爲了簡化問題,咱們假設表裏面只有兩條數據:

file

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

訂單號爲4321的訂單狀態爲1【開啓】

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

file

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

file

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

file

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

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

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

準備開發

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

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

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

file

而後改造一下對應的xml:

file

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

開始自測,遇到問題

請作好單元測試,即便這個功能很是簡單,顯而易見,你信心十足,可是作好單元測試,是一個程序員應有的職業素養。

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

file

按照咱們如今表裏的數據,咱們預期的結果是各自查詢出一條數據。

file

運行起來,咱們一塊兒看看執行結果:

file

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

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

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

file

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

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

不要急着去問搜索引擎。不要慌,要分析,冷靜分析以後纔有收穫。

分析問題

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

因此咱們在application.properties裏面加上這行配置:
logging.level.com.xxxx.xxxx.mapper = debug
注:上面的xxxx換成本身的mapper包的路徑

file

加上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
打印出來的日誌是這樣的(截取了其中一部分):

file

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

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

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

經過idea的Debug模式,咱們能夠獲得從程序運行開始,到斷點處的整個調用鏈路。(若是下面的圖片看不清楚,能夠點開查看大圖):

file

經過調用鏈,日後走三步,咱們能夠看到sql是從boundSql中獲取到的:

file

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

往回走11步,咱們能夠看到boundSql的獲取過程:

file

爲了方便你們找到源碼,我把對應的方法名稱放在這裏: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

file

爲何在mybatis中數字0和空字符串""比返回的是true呢?源碼之下無祕密,繼續往下Debug你會找到這個地方:
org.apache.ibatis.ognl.OgnlOps#doubleValue

file

這裏返回以後,真正的對比是在這裏:

file

v1和v2最終都變成了0.0。因此返回了true。

因爲OgnlOps.equal(0,"")返回爲true,因此整個表達式【OgnlOps.equal(0,"") ?Boolean.FALSE : Boolean.TRUE】返回的是FALSE。

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

v1=0是哪裏來的?

v2=""是從哪裏來的?

返回FALSE會帶來什麼問題?

file

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

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

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

file

而這行代碼,就是回答咱們以前提出問題的後半部分,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標籤修改成這樣便可:

file

或者改爲這樣:

file

再看看執行結果:

file

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

可是,你再回過頭的想想,我最開始的改造mapper.xml是怎麼操做的:
改造點很簡單,在xml文件裏面ctrl+c一下原來的if標籤,再ctrl+v出來改改裏面的名字就行了。

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

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

同時,我也感謝本身作了單元測試,否則測試同窗測試的時候拋出這樣的問題,我會以爲他不會用,他會以爲我是弱雞。

最後說幾句

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

file

而後還有其餘的大量文章都只是扔給你一個解決方法,並無寫爲何這樣寫就能夠解決這個問題。而這樣的搜索結果在我看來是不完美的,由於很難留下深入的印象,致使你或者你同事再次遇見這個問題的時候你會說:哦,這個問題呀,我以前遇見過。怎麼解決的,我給忘了。

你這不廢話嗎?

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

file

後來我把這個問題分享在羣裏以後,羣裏一個朋友也給我分享了一篇文章,肥朝大佬寫的《還有這種操做?淺析爲何要看源碼》。文中給出了另外一種解決方案,有理有據,簡明扼要,是一篇很好的文章,你們能夠看看。

file

尾聲

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

我更加但願的是,當你碰到這個問題,本身分析完了,在網上查詢的時候看到了個人這篇文章。由於本身分析出來的,永遠是印象最深入的,其餘的文章只是起點綴做用。

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

若是你以爲文章還不錯,你的點贊、留言、轉發、分享、讚揚就是對我最大的鼓勵。

感謝您的閱讀,感謝您的關注。

以上。

持續輸出原創文章,更多精彩原創文章,能夠關注公衆號後查看哦。

歡迎關注公衆號【why技術】。在這裏我會分享一些技術相關的東西,主攻java方向,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評。願你我共同進步。

公衆號-why技術

相關文章
相關標籤/搜索