這是why技術的第14篇原創文章前端
在實際開發過程當中我踩到了mybatis的一個坑,我以爲值得記錄、分享一下。java
先說說這個坑是什麼吧。若是你踩過這個坑,而且知道具體的緣由,那這篇文章能夠加深你的印象。若是你沒有踩過,那你可得好好看看,由於你總會遇到的。程序員
具體以下:在mybatis中的OgnlOps.equal(0,"")返回的是true。面試
首先這裏返回爲true就違背了咱們的常識,其次返回爲true,會帶來什麼問題呢?sql
看完本文你就清楚了。apache
本文會按照遇到問題 --> 分析問題 --> 解決問題的行文思路,用追蹤源碼的方法,對這個問題進行剖析。編程
同時分享一下我是怎麼用逆向排查的方法,經過Debug模式找到最關鍵的那一行源碼,而後明白來龍去脈,最後解決這個問題的。segmentfault
本文源碼:mybatis 3.5.3版本。session
先鋪墊一下背景,模擬一個需求。mybatis
有一個訂單表,表結構以下:
爲了簡化問題,咱們假設表裏面只有兩條數據:
訂單號爲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方向,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評。願你我共同進步。