最近在看公司項目時發現有的項目mybatis是基於註解開發的,而我個人的習慣是基於xml文件開發。
對於mybatis註解開發的原理理解不夠,於是翻閱了部分源碼,寫下此文。主要介紹了mybatis開發的兩種形式、三種寫法。還有一點瞎思考,介紹了一處騷代碼、還有一個坑。
原創不易,感謝閱讀,感謝關注,感謝點讚,感謝轉發。
兩種形式,三種寫法
最近在看公司的一些項目的時候發現有的項目裡面的 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 = 18annotationQueryAgeByName whyAge = 18classQueryAgeByName whyAge = 18
測試用例就演示完成了,是一個極簡的用例。
我就是基於這個案例去分析源碼的,在分析之前,其實有點經驗的老哥也能看出來了,我們先撇開常規的 xml 文件的形式不談。
基於 @Select 註解的接口, SQL 就在註解裡面,所以我們只需要通過反射取出註解裡面的 SQL 進行分析就行了。
基於 @SelectProvider 註解的接口,SQL 雖然在一個類的方法中,但是註解上都告訴你是哪個類的哪個方法了,所以,一定是基於反射去取出方法裡面的 SQL 的。
接下來,我們就是去驗證一下。
小心求證
首先,我先問你一個問題。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 !=nulland 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 語句。
進入 getBoundSql 我們可以看到第292行,就是通過 sqlSource 的 getBoundSql 方法獲取到的 boundSql 對象:
org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
這不就又呼應上了嗎?又看到 sqlSource 了。
所以,接下來,我們看一下這兩個方法就可以了:
org.apache.ibatis.builder.StaticSqlSource#getBoundSqlorg.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 去接收了:
但是接口調用時的返回值我手賤寫成了這樣:
明白了吧,妥妥的,空指針,沒得跑了。