目錄
報警原因定位
MyBatis升級 3.2.4版本的官方 Release公告
以版本3.2.3為例,MyBatis構建 SQL陳述句程序的原理分析
以版本3.2.4為例,相比版本3.2.3,MyBatis構建SQL陳述句程序的變化分析
總結
MyBatis上線前后的版本:上線前(3.2.3)上線后(3.4.6)
服務上線后,開始陸續出現了一些更新系統互動日志方面的報警,這屬于系統的輔助流程,報警如下代碼所示,我們發現都是跟 MyBatis相關的報警,說明在進行型別轉換 [ibatis.type.TypeException]的時候,系統產生了強轉錯誤,
更新開票請求回傳日志, 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
報警這一塊代碼,屬于歷史功能,如果失敗并不會影響主流程,但在定位期間,如果頻繁報警的話,就會造成一定的干擾,因此,我們馬上采取了回滾操作,直至報警消失,然后再進行問題的定位和分析,
報警原因定位
在回滾完畢后,我們開始具體分析報警產生的主要原因,于是進行了以下幾步的排查,
第一步,查看了報警的 Mapper方法,如下所示:接收引數,根據主鍵ID更新具體回應內容和時間,入參有3個,型別分別為long、String和 LocalDateTime,
int updateResponse(@Param("id")long id, @Param("response")String response, @Param("updateTime")LocalDateTime updateTime);
第二步,我們查看了 Mapper方法對應的 XML檔案,如下代碼段所示,對應的 parameterType型別是String,而實際引數的型別包括 long、String和 LocalDateTime,
<update id="updateResponse" parameterType="java.lang.String">
UPDATE business_log
SET response = #{response}, update_time = #{updateTime}
WHERE id = #{id}
</update>
第三步,報警的內容是:MyBatis在處理 SQL陳述句時,發現不能將 LocalDateTime轉型為String,這一段邏輯在上線前是可以正常運行的,并且上線的業務邏輯對這段歷史代碼無改動,因此,我們猜測是因為 MyBatis的版本發生了變化導致的,對某些歷史功能不再支持了,MyBatis上線前后的版本:上線前(3.2.3)上線后(3.4.6)
第四步,我們通過第三步可以得到,MyBatis的版本直接升了兩個大版本,因此我們可以基本將原因猜測為 MyBatis升級跨度較大,導致部分歷史功能沒有兼容支持,從而引起線上 SQL的更新報錯,
第五步,為了具體驗證第四步的想法,我們通過 UT的方式,將 MyBatis的版本不斷從 3.4.6往下降,直至沒有報錯的位置,最終的定位是:當 MyBatis版本為3.2.3時,線上代碼是正常可用的,但只要升一個版本,也就是自 3.2.4開始,就開始不兼容目前的用法,不過,我們當時的思路并不是很好,應該從小版本逐個往上升或者使用二分法,可以加速定位版本的效率,
最后,我們定位到了產生報警的根本問題,MyBatis自 3.2.4開始就不支持目前系統內的 SQL Mapper的用法,因此在升級后,線上就出現了頻繁報警的問題,問題已經定位,但是還有很多事情我們需要弄清楚,為什么版本升級后就不兼容歷史的用法?具體是哪一塊內容不兼容?背后的原理又是什么?下文,我們會詳細進行分析,
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為例,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,name,age FROM student WHERE id = 2,MyBatis要將 SQL陳述句完整替換成帶引數值的版本,需要經歷框架初始化以及實際運行時動態替換這兩個部分,因為 MyBatis的代碼非常多,接下來我們主要闡釋和本次案例相關的內容,
在框架初始化階段,主要包括以下流程,如下圖所示:
在框架初始化階段,有一些組件會被構建,逐一做個簡單的介紹:
【1】SqlSession:作為 MyBatis作業的主要頂層API,表示和資料庫互動的會話,完成必要的資料庫增刪改查功能,
【2】資料庫增刪改查功能:負責根據用戶傳遞的 parameterObject,動態地生成 SQL陳述句,將資訊封裝到 BoundSql物件中,并回傳,
【3】Configuration:MyBatis所有的配置資訊都維持在 Configuration物件之中,
接下來,我們主要關注 SqlSource,這個類會負責生成SQL陳述句,這也是本次案例中,3.2.3和3.2.4差異比較大的一個地方,下面,我們會介紹一些原始碼,
在構建 Configuration的程序中,會涉及到構建對應每一條 SQL陳述句對應的 MappedStatement,parameterTypeClass就是根據我們在 XML配置中寫的 parameterType轉換而來,值為 java.lang.String,在構建 SqlSource時,傳入這個引數,如下圖所示:
在 SqlSource的構建中,parameterType引數其實是被忽略不用的,并沒有繼續往下傳遞,這跟官方的描述是一致的,因為 3.2.4之前這個 parameterType屬性被忽略了,然后就創建了 DynamicSqlSource,這個類主要是用于處理 MyBatis動態 SQL的類,如下圖所示:
在框架初始化的階段,需要介紹的內容,在 3.2.3版本已經介紹完畢,當執行 getStudentById方法時,MyBatis的流程如下圖所示,因受限于圖片長度,我們對布局進行了一些調整:
在具體執行階段,也涉及到一些組件,我們需要做簡單的了解:
【1】SqlSession:作為 MyBatis作業的主要頂層API,表示和資料庫互動的會話,完成必要資料庫增刪改查功能,
【2】Executor:MyBatis執行器,這是 MyBatis調度的核心,負責 SQL陳述句的生成和查詢快取的維護,
【3】BoundSql:表示動態生成的 SQL陳述句以及相應的引數資訊,
【4】StatementHandler:封裝了JDBC Statement操作,負責對JDBC statement的操作,如設定引數、將 Statement結果集轉換成 List集合等等,
【5】ParameterHandler:負責對用戶傳遞的引數轉換成 JDBC Statement 所需要的引數,
【6】TypeHandler:負責 Java資料型別和 JDBC資料型別之間的映射和轉換,
我們主要關注獲取 BoundSql以及引數化陳述句的流程,這也是3.2.3和3.2.4差異比較大的一個地方,在進入 Executor的 Query方法后,會首先通過對應的 MappedStatement來獲取 BoundSql,用來幫助我們動態生成SQL陳述句,里面系結了對應的 SQL以及引數映射關系,在構建框架階段,我們使用的 SqlSource是 DynamicSqlSource,通過這個類來生成獲取 BoundSql,如下圖所示:
通過上圖的代碼,我們可以得知,parameterType在初始化階段未被使用,而是在 SQL執行時獲取到的,但獲取到的型別是 parameterObject對應的型別,這個類是用來記錄 Mapper方法上對應的引數,如下圖所示,它并非在 SQL組態檔中標注的java.lang.String,
然后我們通過 SqlSourceBuilder的 parse方法對 SQL以及獲取到的型別進行再次處理,其中的流程代碼比較長,在這個程序中,我們主要去構建 SQL的引數和 Java型別的系結關系,MyBatis依賴這個系結關系,使用對應的 TypeHandler去進行值的轉換,
呼叫鏈路是SqlSourceParser.parse -> 內部類 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping,如下圖中的代碼所示,因為當前的 parameterType為 MapperMethod$ParamMap,經過了多個if判斷,判定當前 property id的propertyType為 Object.class型別,接下來,構建 SQL的引數和 Java型別的系結關系 ParameterMapping,再進行回傳,
構建完成的 ParameterMapping的結構如下圖中的代碼所示,引數id對應的 javaType型別為 java.lang.Object,對應的 TypeHander處理器為 UnknownTypeHandler,也就是未找到合適的 TypeHandler的兜底選項,
接下來,流程就會流轉到Executor,在org.apache.ibatis.executor.SimpleExecutor#doQuery進行查詢時,會根據當前的SQL型別,生成對應的StatementHandler,因為我們目前都是用的預編譯SQL,因此生成的statementHandler就是 PreparedStatementHandler,熟悉 JDBC的小伙伴應該馬上可以猜到對應的陳述句是什么型別了,然后,我們對這句 SQL陳述句進行填充,如下圖中的代碼所示,我們會通過PreparedStatementHandler的 parameterize方法對 Statement進行引數化,也就是進行填充,

在 PreparedStatementHandler進行引數化時,會將引數化的職責交給 DefaultParameterHandler處理,如下圖中的代碼所示,我們主要關注紅線部分,首先會獲取 ParameterMapping對應的 TypeHander,如前文所述,獲取到的是UnknownTypeHandler,然后會通過setParameter方法,將引數id替換成對應的值,

在Typehandler的流程里,首先會進入BaseTypeHandler,然后在具體設定時,會進入子類的方法,在UnknownTypeHandler,首先會再次對引數 parameter進行決議,判斷最正確的 TypeHandler型別,如下圖中的代碼所示:

在 resolveTypeHandler方法中,因為已知了引數值的型別,通過 Integer這個 class在 typeHandlerRegistry中尋找對應的TypeHandler,TypeHandlerRegistry是 MyBatis啟動時內置好的,代表 Java物件型別和 TypeHandler的映射關系,有興趣的同學可以進入這個類詳細看下,在這個例子中,我們會直接獲取到 IntegerHandler,如下圖中的代碼所示:

在獲取到 IntegerHandler后,我們就可以使用 IntegerTypeHandler的setInt方法,對SQL陳述句中的引數進行替換,如圖中的代碼所示,SQL陳述句被成功替換:

后續就是執行 SQL并處理回傳結果,這就不在本文的討論范圍內了,從上文的分析中,我們可以了解到,在3.2.3及以下版本,MyBatis會忽略 parameterType,在真正進行SQL轉換時,重新根據SQL方法入參型別,然后計算合適的 TypeHandler處理器,所以本案例中的代碼在3.2.3版本時,它在運行時是正常的,
以版本3.2.4為例,相比版本3.2.3,MyBatis構建SQL陳述句程序的變化分析
在前一章節中,我們得知 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的構造方法,

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

構建 ParameterMapping與3.2.3版本的差異
因為在框架初始化階段,SqlSource的 ParameterMapping中id對應的型別就是 java.lang.String,這就導致在進行 SQL陳述句的替換時,獲取到的 TypeHandler是 StringTypeHandler,如下圖所示:

整數型別的引數獲取到了StringTypeHandler
后面的報錯原因就比較好理解了,在呼叫StringTypeHandler的 setString方法時,報出了java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 的錯誤,
總結
MyBatis 3.2.3版本支持 parameterType和實際引數型別不匹配,在執行 SQL階段,動態計算值處理器型別,在大版本升級2個版本號后,parameterType實際的型別開始生效,使用對應這個型別的 TypeHandler對SQL進行引數替換,會導致 Mapper方法中的引數和 XML中的 parameterType不匹配時,進而會出現型別轉換報錯,
這一段排查的經歷,對自己后續撰寫代碼及在系統上線時也有一些啟發,主要包括以下幾個方面:
【1】在專案升級時,需要線下進行全面回歸,要避免框架存在不兼容的用法,不然的話,就容易導致線上錯誤,
【2】開發同學可以檢查自己系統內的 MyBatis版本,如果是3.2.4以下,需要全面檢查下現在的 Mapper檔案里對于 parameterType的使用和 Mapper方法中實際的引數型別是否一致,避免升級到3.2.4及以上版本時發生轉型報錯,如果有不匹配的情況存在,需要進行修正或者不使用 parameterType,讓 MyBatis在運行 SQL時自動計算對應的型別,
【3】可以考慮使用 MyBatis-Generator來自動生成 XML和 Mapper檔案,畢竟是專業團隊在維護,穩定性相對來說會更好一些,同時能夠避免手動修改 XML檔案帶來的誤操作,
【4】可以主動關注強依賴的一些開源框架的 Release Log,不要錯過了重要的資訊,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/202694.html
標籤:其他
上一篇:2020淘寶雙11自動刷喵幣腳本
