1. 程式人生 > 程式設計 >淺談Mybatis版本升級踩坑及背後原理分析

淺談Mybatis版本升級踩坑及背後原理分析

1、背景

某一天的晚上,系統服務正在進行常規需求的上線,因為釋出時,提示統一的pom版本需要升級,於是從 1.3.9.6 升級至 1.4.2.1。
當服務開始上線後,開始陸續出現了一些更新系統互動日誌方面的報警,屬於系統輔助流程,報警下圖所示,具體系統資料已脫敏,內容是Mybatis相關的報警,在進行型別轉換的時候,產生了強轉錯誤。

更新開票請求返回日誌,id:{#######},response:{{"code":XXX,"data":{"callType":3,"code":XXX,"msg":"XXXX","shopId":XXXXX,"taxPlateDockType":"XXXXXXX"},"msg":"XXXXX","success":XXXX}}

nested execption is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='updateTime',mode=IN,javaType=class java.lang.String,
jdbcTyp=null,resultMapId='null',jdbcTypeName='null',expression='null'}.Cause org.apache.ibatis.type.TypeException,Error setting non null parameter #2 with JdbcType null. Try setting a
different Jdbc Type for this parameter or a different configuration property.Cause java.lang.ClassCastException:java.time.LocalDateTime cannot be cast to java.lang.String

報警的一塊程式碼,屬於歷史功能,失敗並不會影響主流程,但在定位期間,會頻繁報警,造成一定的干擾,因此當時首先採取回滾操作,將統一的pom版本回滾至歷史版本,報警消失,再進行問題的定位和分析。
以下章節是對報警原因的定位及原因詳細分析的介紹。

2、報警原因定位

首先是具體的報警原因:

由於mybatis版本由inf-bom引入而來,在inf-bom升級後,由3.2.3 升級至了 3.4.6版本,而Mybatis自3.2.4開始就不支援目前系統內的SQL Mapper的用法,因此上線後,線上出現頻繁報警。接下來是定位的過程。

回滾完畢後,開始具體分析報警產生的主要原因,進行了以下幾步的排查。

1.查看了報警的Mapper方法,如下程式碼所示, 這個是接收返回引數,根據主鍵id,更新具體響應內容和時間的程式碼,入參有3個,型別分別為long,String 和 LocalDateTime

int updateResponse(@Param("id")long id,@Param("response")String response,@Param("updateTime")LocalDateTime updateTime);

2.查看了Mapper方法對應的XML檔案,如下程式碼,對應的parameterType型別是String,而實際引數的型別有Long,有String,也有LocalDateTime。

<update id="updateResponse" parameterType="java.lang.String">
UPDATE invoice_log
 SET response = #{response},update_time = #{updateTime}
WHERE id = #{id}
</update>

3.查看了Mybatis上線前後的版本,因為報警的內容是Mybatis處理sql語句時,發現不能將LocalDateTime轉型為String,這一段邏輯在上線前是ok的,上線的業務邏輯對這段歷史程式碼無改動,因此猜測是統一pom的升級,導致Mybatis的版本發生了變化,某些歷史功能不支援了。 mybatis版本上線前後的變化,1.3.9.6對應的版本是3.2.3,1.4.2.1對應的版本是3.4.6。

4.通過第3步可以得到,在這次inf-bom的版本升級中,mybatis3的版本直接升了兩個大版本,因此可以基本將原因猜測為 Mybatis升級跨度大,導致部分歷史功能沒有相容支援,引起的線上sql更新報錯。

5.為了具體驗證第4步的想法,通過UT的方式,通過將Mybatis的版本不斷從3.4.6往下降,直至沒有報錯位置,最終定位是Mybatis版本為3.2.3時,線上程式碼是正常可用的,只要升一個版本也就是自3.2.4開始,就開始不相容目前的用法。(這個當時思路不是很好,應該從小版本逐個往上升,可以去加速定位版本的效率)

最後定位報警原因,由於mybatis版本由統一pom引入而來,在統一pom升級後,由3.2.3 升級至了 3.4.6版本,而Mybatis自3.2.4開始就不支援目前系統內的SQL Mapper的用法,因此上線後,線上出現頻繁報警。

報警原因已定位,但為什麼版本升級後就不相容歷史的用法,並且具體不相容的是哪一塊內容,背後的原理又是什麼,請看接下來章節的詳細分析。

3、詳細分析

3.1 Mybatis 升級3.2.4版本的官方Release公告

首先從報錯的原因上來看,Caused by: java.lang.ClassCastException: java.lang.LocalDateTime cannot be cast to java.lang.String ,是Mybatis在構建sql語句時,發現時間欄位 型別為LocalDateTime 不能強制轉為String型別。 這個SQL XML的配置在3.2.3的版本是正常可以用,那麼首先是從Mybatis 的 release log上檢視3.2.4版本 發生了什麼變化。

An special remark about this feature. Previous versions ignored the "parameterType" attribute and used the actual parameter to calculate bindings. This version builds the binding information during startup and the "parameterType" attribute is used if present (though it is still optional),so in case you had a wrong value for it you will have to change it.

從官網的Release Log可以看出,Mybatis在3.2.4以前的版本,是忽略XML中的parameterType這個屬性,並且使用真實的變數型別進行值的處理,在3.2.4及以後的版本中,這個屬性會被啟用,因此如果出現型別不匹配的話,就會出現轉型失敗的報錯,也提示我們開發者在升級到這個版本及以上時,需要檢查系統內的XML配置,使型別相匹配,或者不設定該屬性,讓Mybatis自行進行計算。

從以上內容,可以瞭解到,在版本升級後,mybatis在構建sql語句,獲取欄位值的時候邏輯發生了變化,那麼接下來通過一個普通的示例,瞭解mybatis在獲取欄位值這一塊的具體程式碼流程是怎樣的,以3.2.3版本為例。

3.2 以版本3.2.3為例,mybatis構建SQL語句過程的原理分析

首先,先看以下配置,定義了一個通過主鍵id獲取學生資訊的方法,仿造系統內的歷史程式碼,也將parameterType定義為 java.lang.String 和 方法對應的引數 int 並不相同。

public StudentEntity getStudentById(@Param("id") int id);

<select id="getStudentById" parameterType="java.lang.String" resultType="entity.StudentEntity">
SELECT id,name,age FROM student WHERE id = #{id}
</select>

mybatis框架要做的事情就是在執行getStudentById(2)的時候,將 #{id}進行替換,使SQL語句變成 SELECT id,age FROM student WHERE id = 2 。Mybatis要將SQL語句完整替換成帶引數值的版本,需要經歷框架初始化以及實際執行時動態替換兩個部分。因為Mybatis的程式碼非常多,接下來主要闡釋和本次案例相關的內容。

在框架初始化階段,主要有以下流程,如下圖所示

淺談Mybatis版本升級踩坑及背後原理分析

在框架初始化階段,有一些元件會被構建,接下來進行逐一做個簡單的介紹:

  • SqlSession 作為MyBatis工作的主要頂層API,表示和資料庫互動的會話,完成必要資料庫增刪改查功能。
  • SqlSource 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回。
  • Configuration MyBatis所有的配置資訊都維持在Configuration物件之中。

接下來主要關注SqlSource,這個類會負責在負責生成SQL語句,也是本次案例中,3.2.3和3.2.4差異比較大的地方。接下來會一些原始碼部分的介紹。

在構建Configuration的過程中,會涉及到構建對應每一條sql語句對應的MappedStatemnt,在parmeterTypeClass就是根據我們在xml配置中寫的parmeterType轉換而來,值為java.lang.String,在接下來構建SqlSource中,傳入了這個引數,如下圖所示:

淺談Mybatis版本升級踩坑及背後原理分析

在SqlSource的構建階段中,parameterType引數其實是被忽略不使用的,這也和官方的描述是一致的,3.2.4之前這個parameterType屬性是被忽略的,然後建立了DynamicSqlSource,這個類主要是用於處理Mybatis動態Sql的類。

淺談Mybatis版本升級踩坑及背後原理分析

在框架初始化階段,需要介紹的內容,在3.2.3版本已經介紹完畢,接下來是當執行getStudentById方法時,Mybatis的流程,如下圖所示,受限於圖片長度,進行了佈局的調整:

淺談Mybatis版本升級踩坑及背後原理分析

在具體執行階段,也有一些元件,我們需要做了解

SqlSession 作為MyBatis工作的主要頂層API,表示和資料庫互動的會話,完成必要資料庫增刪改查功能

Executor MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護

BoundSql 表示動態生成的SQL語句以及相應的引數資訊

StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數、將Statement結果集轉換成List集合。

ParameterHandler 負責對使用者傳遞的引數轉換成JDBC Statement 所需要的引數

TypeHandler 負責java資料型別和jdbc資料型別之間的對映和轉換

接下來主要關注在獲取BoundSql以及引數化語句的流程,也是本次案例中,3.2.3和3.2.4差異比較大的地方。接下來會一些原始碼部分的介紹。

在進入Executor的query方法後,會首先通過對應的MappedStatement獲取BoundSql,用來幫助我們動態生成SQL語句,裡面綁定了對應的SQL以及引數對映關係,在構建框架階段,我們使用的SqlSource是DynamicSqlSource,通過這個類來生成獲取BoundSql。

淺談Mybatis版本升級踩坑及背後原理分析

通過上圖的程式碼可以得知,parameterType在初始化階段未被使用,而是在SQL執行時,獲取到的,但獲取到的型別是parameterObject對應的型別,這個類是用來記錄mapper方法上對應的引數的。如下圖所示,並非在Sql配置檔案中標註的java.lang.String。

淺談Mybatis版本升級踩坑及背後原理分析

接下來,通過SqlSourceBuilder sqlSourceParser 對sql以及計算得到的型別進行再次處理,當中流程程式碼比較長,主要是在這個過程中去製作 sql方法的入參 和 java型別的繫結關係,mybatis依賴這個繫結關係使用對應的TypeHandler去進行值的轉換,呼叫鏈路是SqlSourceParser.parse -> 內部類 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping, 如下圖程式碼所示。因為當前的parmeterType為 MapperMethod$ParamMap,進過了多個if判斷,判定當前property id 的 propertyType 為Object.class型別,接下來就是製作 sql方法的入參 和 java型別的繫結關係 parameterMapping,並進行了返回。

淺談Mybatis版本升級踩坑及背後原理分析

製作完成的ParameterMapping的結構如下圖程式碼所示,引數id對應的javaType型別為 java.lang.Object,對應的TypeHander處理器為UnknownTypeHandler,也就是未找到合適的TypeHandler的兜底選項。

淺談Mybatis版本升級踩坑及背後原理分析

接下來流程就會流轉到Executor,org.apache.ibatis.executor.SimpleExecutor#doQuery進行查詢時,會根據當前的SQL型別,生成對應的statmentHandler,因為我們目前都是用的預編譯SQL,因此生成的statementHandler就是PrepareStatmentHandler,熟悉JDBC的小夥伴應該馬上可以猜到這對應的語句是什麼型別了。接下來就會對這句SQL語句進行填充,如下圖程式碼所示,會通過PrepareStatmentHandler的parameterize方法對Statment進行引數化,也就是進行填充過程。

淺談Mybatis版本升級踩坑及背後原理分析

在PreparseStatmentHandler進行引數化時,會將引數化的職責交給DefaultParameterHandler進行,如下圖程式碼所示,主要關注紅線部分,首先會獲取parameterMapping對應的TypeHander,如上章節所示,獲取到的是UnknownTypeHandler,然後會通過setParameter方法,將引數id替換成對應的值。

淺談Mybatis版本升級踩坑及背後原理分析

在typehandler的流程裡,首先會進入BaseTypeHandler,然後在具體設定時,進入子類的方法,在UnknownTypeHandler,首先會再次對parameter進行解析,判斷最正確的TypeHandler型別,如下圖程式碼所示:

淺談Mybatis版本升級踩坑及背後原理分析

在resolveTypeHandler方法中,因為已知引數值的型別,通過Integer這個class在typeHandlerRegistry中尋找對應的TypeHandler,TypeHandlerRegistry是Mybatis啟動時內建好的,java物件型別和TypeHandler的對映關係,有興趣的可以進這個類詳細看下,在本案例中,會直接獲取到IntegerHandler,如下圖程式碼所示:

淺談Mybatis版本升級踩坑及背後原理分析

在獲取到IntegerHandler後,就可以使用IntegerTypeHandler的setInt方法,對SQL語句中的引數進行替換,如下圖程式碼所示,sql語句被成功替換。

淺談Mybatis版本升級踩坑及背後原理分析

後續就是執行SQL並處理返回結果,不在本文的討論範圍內,從上文的分析中,我們可以瞭解到,在3.2.3及以下版本,Mybatis會忽略parmeterType,在真正進行sql轉換時,重新根據sql方法入參型別計算合適的TypeHandler處理器,所以本案例中的程式碼在3.2.3時執行時正常的。

3.3 以版本3.2.4為例,相比版本3.2.3,mybatis構建SQL語句過程的變化分析

在3.2章節中,得知mybatis是在執行sql階段重新計算引數對應的TypeHandler進行sql引數替換,那麼在版本3.2.4中,mybatis做了什麼改動,導致了原有的使用方式不可用了呢。從官方的release log來看,版本3.2.4做了這樣一個改動。

This version builds the binding information during startup and the "parameterType" attribute is used

意思是說 parameterType會在框架執行階段就被使用到,從這個中,我們將分析的重點放在構建階段,同時負責處理繫結關係的BoundSql由配置階段的SqlSource生成,因此主要檢視SqlSource的構建,3.2.4發生了什麼變化,如下圖所示。與3.2.3不同,3.2.4首先判斷了是否為動態SQL,在非動態SQL情況下,將parameterType java.lang.String作為引數,傳入了SqlSource的構造方法。

淺談Mybatis版本升級踩坑及背後原理分析

後續流程與3.2.3一致,因為parameter型別為java.lang.String,在構建parameterMapping時,使用的型別就是java.lang.String。

淺談Mybatis版本升級踩坑及背後原理分析

因為在框架初始化階段,SqlSource中 parameterMapping, id對應的型別就是java.lang.String,導致在進行Sql語句替換時,獲取到的TypeHandler是StringTypeHandler,如下圖所示:

淺談Mybatis版本升級踩坑及背後原理分析

後面的報錯原因就比較好理解了,在呼叫StringTypeHandler的setString方法時,報出了java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String的錯誤。

4、總結

總結一下這個案例的主要原因是:

mybatis 3.2.3版本 相容parameterType和實際引數型別不匹配,執行時動態計算值處理器型別,在大版本升級2個版本號後,parameterType開始生效,以parameterType作為引數的實際型別進行TypeHandler的獲取計算,導致型別不匹配時,強轉報錯。

帶給我自己的在後續編寫編寫程式碼及系統上線方面的啟示是:

1.在統一pom升級時,需要線下進行全面迴歸,避免框架存在不相容的用法,導致線上錯誤。

2.開發同學可以檢查自己系統內的mybatis版本,如果是3.2.4以下,需要全面檢查下現在的mapper檔案裡 對於parameterType的使用 和實際的引數型別是否一致,避免升級到3.2.4及以上版本時發生相容報錯,如果有不匹配的情況存在,需要進行修正 或者 不使用parameterType,讓Mybatis在執行SQL時自動計算對應的型別,

3.可以考慮使用mybatis-generator來自動生成xml和mapper檔案,有專業團隊維護,相對來說穩定性更好,也避免自己手動修改xml檔案容易帶來誤操作。

4.可以主動關注強依賴的一些開源框架的Release log,有很多重要的資訊。

到此這篇關於淺談Mybatis版本升級踩坑及背後原理分析的文章就介紹到這了,更多相關Mybatis版本升級內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!