mybatis開發,你用 xml 還是註解?我 pick ...
最近在看公司專案時發現有的專案mybatis是基於註解開發的,而我個人的習慣是基於xml檔案開發。
對於mybatis註解開發的原理理解不夠,於是翻閱了部分原始碼,寫下此文。主要介紹了mybatis開發的兩種形式、三種寫法。還有一點瞎思考,介紹了一處騷程式碼、還有一個坑。
原創不易,感謝閱讀,感謝關注,感謝點贊,感謝轉發。
荒腔走板
大家好,我是 why 。老規矩,在技術分享開始之前,先荒腔走板,聊點別的。
上週我寫的這篇文章《我告訴你這書的第 3 版到底值不值得買?》居然被《深入理解Java虛擬機器》的作者周志明先生看到了,還給我讚賞並留言給我說:作者表示感謝,真心的。
說實話,我看到這個讚賞的時候我都震驚了。有一種和大神產生了交集的感覺。
其實上週這篇文章是出版社找到我說送我一本第三版,讓我看看,然後寫個觀後感就行。
恰好,在他們沒有找到我之前我也是有這樣的打算的。
我不是為了白嫖出版社幾本書,而是我早在去年年底打算買第三版後,這篇文章就一直在著手準備了。
在機緣巧合之下,即完成了自己的計劃,又獲得了出版社的幾本書,通過出版社,又勾搭上了本書作者,不僅獲得了作者的讚賞還得到一本簽名版。
哎,這瘋狂而又操蛋的人生呀。
所以你問我寫了這麼久的文章收穫了什麼?
說實在的,我沒有通過寫文章掙到幾個錢。但是我收穫的是與一群志同道合的原創作者同行的機會、是讀者讀完文章後對我的文章的指點與讚揚、是偶爾與幾位業界大佬之間互動的驚喜。
僅此而已。
好了,說迴文章。
兩種形式,三種寫法
最近在看公司的一些專案的時候發現有的專案裡面的 mybatis 是基於註解開發的。而我個人的習慣是基於 xml 檔案開發。
所以對於基於註解開發的原理不太瞭解,於是去翻看了一下相關原始碼,形成此文。
本文主要介紹基於 mybatis 開發的兩種形式,三種寫法。
其中兩種形式是指:
1.基於 xml 檔案。
2.基於註解開發。
三種寫法是指除了 xml 的形式外,註解又有兩種不同的寫法,它們的實現原理也略有不同,拿 Select 語句舉例,就有兩種註解 @Select、@SelectProvider 。
演示示例
先上一個演示示例給大家直觀的感受一下:
首先,我們有個使用者表,包含這些欄位和這樣一條資料:
然後我們搞個介面類,用三種方式去查詢使用者的年齡,具體如下:
xmlQueryAgeByName 方法是使用 xml 的方法去查詢使用者年齡,對應的 xml 如下:
annotationQueryAgeByName 方法是使用 @Select 註解去查詢使用者的年齡,SQL 就寫在註解裡面:
classQueryAgeByName 方法是使用 @SelectProvider 註解去查詢使用者的年齡,可以看到註解裡面有個 type 欄位,對應一個 class 類。一個 method 欄位,對應 class 類中的一個方法:
其中 UserInfoSql 類如下:
然後,再來一個測試用例,把三個方法都測試一下:
最後的輸出結果如下:
xmlQueryAgeByName whyAge = 18
annotationQueryAgeByName whyAge = 18
classQueryAgeByName whyAge = 18
測試用例就演示完成了,是一個極簡的用例。
我就是基於這個案例去分析原始碼的,在分析之前,其實有點經驗的老哥也能看出來了,我們先撇開常規的 xml 檔案的形式不談。
基於 @Select 註解的介面, SQL 就在註解裡面,所以我們只需要通過反射取出註解裡面的 SQL 進行分析就行了。
基於 @SelectProvider 註解的介面,SQL 雖然在一個類的方法中,但是註解上都告訴你是哪個類的哪個方法了,所以,一定是基於反射去取出方法裡面的 SQL 的。
接下來,我們就是去驗證一下。
好,準備發車。
小心求證
關於 mybatis 我之前寫過這篇文章《很開心,在使用mybatis的過程中我踩到一個坑》,其中提到了一個逆向排查法。有興趣的可以去看一下。
在這篇文章中我們還是來個常規分析吧。本文分析原始碼為 mybatis 3.4.0 版本。
首先,我先問你一個問題。SpringBoot 是怎麼載入 mybatis 的?
熟悉 SpringBoot 啟動過程的朋友知道,SpringBoot 會去載入mybatis-spring-boot-autoconfigure-x.x.x.jar下 META-INF 中的spring.factories檔案:
所以,下面的 sqlSessionFactory 方法就是我們的入口處:
入口給你找到了,你可以直接在這裡加上斷點開始 debug 了。
我知道,雖然是剛剛開始,但是可能有些讀者覺得已經超綱了。但是沒有關係的,繼續看下去,我這裡只是給你說個入口在哪而已。
由於 debug 的過程不是文字重點,這裡就不去介紹了。debug 的時候我們會看到這個方法:
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
這個方法的第 92 行,就是我們的 xml 內容:
然後在下面這個方法中對 xml 檔案進行瘋狂的解析:
org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
圖片可以點開看大圖哦,debug 模式,可以看到一些輸出:
上面的原始碼的第 94 行,獲 取 SqlSource 很關鍵,要好好看看,這裡呼叫了這個方法:
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, org.apache.ibatis.parsing.XNode, java.lang.Class<?>)
接著在下面方法的第 52 行,剝離出整個完整的 sql:
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
上面就是常規的 xml 形式的 SQL 原始語句(變數、條件表示式都還未進行替換,不可直接執行的 SQL)獲取過程,不是本文重點,簡單的分析一下就行。
接下來繼續 debug 的時候會遇到下面這個方法,看包名你就知道,這就是我們關心的註解解析相關的方法了:
org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse
在這個方法裡面,會去迴圈處理 mapper 類中的方法:
接下來,就會遇到這個方法了:
org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#getSqlSourceFromAnnotations
當迴圈到 annotationQueryAgeByName 方法的時候,下面方法的一些關鍵引數如下所示:
首先我們看 428 行,解析到了 sqlAnnotationType 為 Select:
所以會進入下面的 if 分支,然後執行到 435 行,通過反射獲取到了 @Select 註解上的 SQL 語句:
繼續往下走,通過 436 行,我們可以走到這個方法:
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, java.lang.String, java.lang.Class<?>)
這個方法就有點意思了,進來判斷了 script 即 SQL 是否是以 script 指令碼開頭的,如果是,則走的和之前 xml 一樣的解析邏輯:
我第一次看到這個地方的時候,一下才恍然大悟過來,我才明白,@Select 的本質還是 xml 檔案的形式啊。只是換了個展現形式而已。
我之前的一個問題,或者說是錯誤的看法也就迎刃而解了。
我之前認為 @Select 的方式是隻能支援簡單 SQL 的書寫,對於一些類似於判空的需求是不支援的。(因為對 mybatis 註解開發確實不熟)
比如在 xml 檔案中這樣去寫:
<when test='startPage !=null and pageSize != null '>
LIMIT #{startPage},#{pageSize}
</when>
只是這個寫法,呃,怎麼說呢,非常不優雅。
不要為了註解而註解,很明顯,這種情況直接用 xml 形式更好。
到這裡,我們也知道了,基於 @Select 註解的方式開發時, mybatis 會通過反射獲取到註解裡面的 SQL ,而這些 SQL 需要一些比較複雜功能,比如判斷條件是否為空時,可以用 script 標籤包裹起來。寫法和在 xml 裡面開發是一樣的。
接下來,我們看看 @SelectProvider 方法是什麼個樣式。
還是在同樣的方法中,只是走向了另外一個分支:
此時的 sqlProviderAnnotation 裡面的東西如下:
接著去 new ProviderSqlSource 物件:
在這個方法中,獲取到了註解上的具體的提供 SQL 原始語句的方法。
注意紅框中框起來的 providerMethod 物件,後面獲取真正執行的 SQL 語句的時候還會用到。
同時,我們可以看到 ProviderSqlSource 是 SqlSource 的實現類。
所以,不管是 xml 還是註解,最終都需要獲取到一個 SqlSource 物件。
而在本文的示例程式碼中, xml 和 @Select 生成的是 RawSqlSource。
@SelectProvider 生成的是 ProviderSqlSource。他們裡面放的東西是不一樣的。
在 RawSqlSource 裡面的 sqlSource 變數(型別 StaticSqlSource)放的已經是從 xml 或者 @Select 註解中獲取到的 SQL 原始語句了(但是裡面的變數還沒替換,因為程式啟動過程中根本不知道變數的值具體是什麼,如果有一些條件表示式的話同理)。
而在ProviderSqlSource 裡面,我們前面已經說了,放的是 @SelectProvider 註解上具體的提供 SQL 語句的方法,僅僅是方法,而不是語句。
前面的所有分析都是在我們的方法真正執行之前,接下來,才會 debug 到我們的測試用例,因為只有我們的測試用例裡面才有真正的入參, mybatis 才能根據入參,執行最終的 SQL 語句。
所以,接下來,我們就是要找到真正生成 SQL 語句的地方,這裡就能和之前文章《很開心,在使用mybatis的過程中我踩到一個坑》中的逆向排查法中得出的結論進行呼應了。
進入 getBoundSql 我們可以看到第292行,就是通過 sqlSource 的 getBoundSql 方法獲取到的 boundSql 物件:
org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
這不就又呼應上了嗎?又看到 sqlSource 了。
所以,接下來,我們看一下這兩個方法就可以了:
org.apache.ibatis.builder.StaticSqlSource#getBoundSql
org.apache.ibatis.builder.annotation.ProviderSqlSource#getBoundSql
首先看一下 StaticSqlSource 的實現:
裡面的一些關鍵引數如下:
首先可以 sql 變數,裡面是一條待加工的 SQL 語句,我們前面已經分析過了,程式啟動的過程中,這裡為什麼不替換呢?
因為不知道換成啥呀。
那你覺得在這個地方會替換嗎?
還是不會的。雖然我們已經告訴 mybatis , userName 就是 why 了,但如果在這個地方把 why 帶到 SQL 裡面去,我們倒是可以獲得一個完整的正確的 SQL。
但是,如果我們傳入的是 “why or 1=1”呢?
這是什麼東西我相信你一下就恍然大悟了吧,SQL 注入呀。
另外插一句,如果想看 SQL 注入的情況,就是走到 DynamicSqlSource 的情況,在 xml 中把 # 換成 $ 就行,有興趣的可以試一試。
我這裡只是給你截個圖,瞅一眼:
好了,我們接著剛才繼續說。
繼續 debug 會走到這方法中去:
org.apache.ibatis.executor.SimpleExecutor#doQuery
而這個方法的第 62 行,prepareStatement,這個東西不用說了吧,從學 JDBC 的時候就用上它了,老朋友了:
最後去執行真正的查詢操作,處理返回值。
接著看 ProviderSqlSource 的實現,注意看我圈起來的那部分的分支判斷:
無非就是判斷有幾個引數,反射方法呼叫的時候需要怎麼傳參而已。最終會呼叫到這個方法裡面來獲取 SQL 語句:
可以看一下這個時候 providerMethod 和 sql 變數分別是什麼:
而這裡這個 providerMethod 怎麼來的知道了吧?我們前面剛剛分析過了。
new ProviderSqlSource 物件的時候,我還專門說了:“注意紅框中框起來的 providerMethod 物件,後面獲取真正執行的 SQL 語句的時候還會用到。”
就是在這個地方用到的。
你看,又呼應上了。
這個時候,我們獲取到了原始的 SQL 語句了,也有引數了,這樣的場景和我們剛剛分析的情況就一模一樣了,所以後面的邏輯都一樣,進行了程式碼複用:
進入第 98 行,也就是下面這個我們之前分析過的方法:
org.apache.ibatis.builder.SqlSourceBuilder#parse
在這個方法中,返回了一個 StaticSqlSource 物件:
再次呼應,流程是一樣一樣的。
另外,再說一下,用 @SelectProvider 註解時的 class 物件裡面的方法還可以這樣去寫,有興趣的可以去研究一下:
好了,我們的論證部分就算是完了,我發現這個東西,用視訊真的幾分鐘就講清楚了,描述起來還是有點困難的,難道是在逼我當UP主嗎?
不知道大家看的是否明白了,如果對 mybatis 瞭解不多的朋友可能看起來有一點吃力,但是沒有關係,你就把這篇文章當做一個導讀,然後自己搞個 Demo 跑起來,玩一玩就行。
個人思考
其實在寫這篇文章的時候我就產生了一個思考。
mybatis 為什麼要去支援註解呢?
當然,我們都知道,基於註解開發是趨勢,給我們簡化了非常多的東西。
特別是 SpringBoot 的出現,可以說是註解開發的黃金時代。
遙想當年剛剛入行的時候,開發一個 SSM 專案大多數時間都是在進行 xml 檔案的配置。
可以說是很羨慕現在入行的小年輕了,沒有真正經歷(也許自己搭建過,玩了一下)過被 xml 配置支配的恐懼。
在 xml 時代,大家都是粘來粘去的。而現在基於註解開發了,很多東西都簡化了,漸漸的自己也能很輕鬆的搭建一個可以跑起來的小專案了。
所以,基於註解開發大體上一件很優雅,很好,很值得推廣的事情。
為什麼說大體上呢?
因為我個人偏見的覺得對於 mybatis 框架來說,沒有 xml 檔案的 mybatis 是沒有靈魂的。
當然,如果你全是簡單的 SQL 語句就能實現的功能,你可以用註解開發。但是這個情況,我覺得還是在少數的。
同樣,我們可以用註解的形式實現所有 xml 檔案能實現的功能。但是我覺得不太優雅。
所以,我覺得一個比較折中的方式是簡單 SQL 可以用註解開發,如果是一些有諸如條件判斷類的需求的 SQL 還是要寫在 xml 檔案中。
不要為了擁抱註解,而完全摒棄了 xml 的形式。
你記得嗎,在 xml 時代轉向註解時代的時候,還有一個經常用到的註解。
有人說這是過渡時代的產物,而在我看來,這更是求同存異的完美體現。
這個註解,就完全的體現最近這句很火的話:
君子美美與共,和而不同。
當然這些都是我在寫這篇文章的過程中產生的一些淺顯的個人看法而已。不具備參考意義。
騷程式碼
另外,再給大家分享一個我認為的 mybatis 的騷程式碼吧。
程式碼非常的簡單明瞭,很久以前第一次看 mybatis 原始碼的時候我就是覺得有點“騷”,給我留下了深刻的影響:
org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
selectOne 方法:
該方法呼叫的還是 selectList 方法,但是對返回集合進行了一個判斷,如果集合大小為 1,說明就真的是 selectOne ,如果大於 1,則丟擲異常。
說真的,如果讓我去實現這個功能,我不會一下就想到這個方法,我會去老老實實的寫功能,然後對返回值進行判斷。寫完之後,我可能才會發現。哎,這段程式碼和 selectList 方法可以複用哦,然後才提取出來,變成這樣。
記得很久之前面試,面試官問我對看過的原始碼中哪段影響深刻的,其中我就說到了這個方法。
總之,我個人覺得很妙。
注意坑
然後再說一個之前踩過的坑吧,還導致了一次緊急上線。
還是拿文中的示例說明:
如果我們把返回值從 Integer 變成 int:
用這個測試用例還是會正常查詢出結果:
但是,如果我們查詢一個數據庫中不存在的人的年齡呢?比如這樣:
那麼就會丟擲這樣的錯誤:
找到對應原始碼,我們可以看到:
當返回值是 null 的時候,但是方法上的返回值型別又不是包裝型別中的一種,也不是 void 型別,則丟擲異常。
看一下這個方法,是 native 的:
java.lang.Class#isPrimitive
你想想為什麼 mybatis 給你進行了這樣的一個判斷呢?
那就是如果返回為 null ,自動拆箱的時候會丟擲空指標的。
即使 mybatis 幫我們擋了一下,我還是完美的踩了一個坑,寫出了空指標異常。
程式碼是這樣的,接收的時候我還是用 Integer 去接收了:
但是介面呼叫時的返回值我手賤寫成了這樣:
明白了吧,妥妥的,空指標,沒得跑了。
最後說一句(求關注)
人的潛力真的是巨大的,不逼自己一把,我都不知道我這周真的可以把文章寫完。
最近這周晚上回來的時間晚一點了,所以周內的晚上一般都沒有時間寫文章。
最近成都也熱起來了,夏天我有時還是需要一小會的午睡,來保證下午的工作狀態的。其他的季節我不需要午休,所以現在中午也很少有時間寫一會文章了。
算了一下,連上這篇,我已經連續周更 39 周,輸出 48 篇原創文章了。
啊,我真是牛皮呀。
周更作者的痛苦與快樂,你想象不到:
點個贊吧,周更很累的,不要白嫖我,需要一點正反饋。
才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,由於本號沒有留言功能,還請你在後臺留言指出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是 why,一個被程式碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。
歡迎關注我的公眾號【why技術】,這號主要進行一些技術分享:
本文使用 mdnice 排版