某一天的晚上,系統服務正在進行常規需求的上線,由於發佈時,提示統一的pom版本須要升級,因而從 1.3.9.6 升級至 1.4.2.1。
當服務開始上線後,開始陸續出現了一些更新系統交互日誌方面的報警,屬於系統輔助流程,報警下圖所示, 具體系統數據已脫敏,內容是Mybatis相關的報警,在進行類型轉換的時候,產生了強轉錯誤。java
更新開票請求返回日誌, 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版本回滾至歷史版本,報警消失,再進行問題的定位和分析。
如下章節是對報警緣由的定位及緣由詳細分析的介紹。程序員
首先是具體的報警緣由:sql
因爲mybatis版本由inf-bom引入而來,在inf-bom升級後,由3.2.3 升級至了 3.4.6版本,而Mybatis自3.2.4開始就不支持目前系統內的SQL Mapper的用法,所以上線後,線上出現頻繁報警。接下來是定位的過程。數據庫
回滾完畢後,開始具體分析報警產生的主要緣由,進行了如下幾步的排查。express
1.查看了報警的Mapper方法,以下代碼所示, 這個是接收返回參數,根據主鍵id,更新具體響應內容和時間的代碼,入參有3個,類型分別爲long, String 和 LocalDateTimeapache
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更新報錯。mybatis
5.爲了具體驗證第4步的想法,經過UT的方式,經過將Mybatis的版本不斷從3.4.6往降低,直至沒有報錯位置,最終定位是Mybatis版本爲3.2.3時,線上代碼是正常可用的,只要升一個版本也就是自3.2.4開始,就開始不兼容目前的用法。(這個當時思路不是很好,應該從小版本逐個往上升,能夠去加速定位版本的效率)app
最後定位報警緣由,因爲mybatis版本由統一pom引入而來,在統一pom升級後,由3.2.3 升級至了 3.4.6版本,而Mybatis自3.2.4開始就不支持目前系統內的SQL Mapper的用法,所以上線後,線上出現頻繁報警。
報警緣由已定位,但爲何版本升級後就不兼容歷史的用法,而且具體不兼容的是哪一塊內容,背後的原理又是什麼,請看接下來章節的詳細分析。
首先從報錯的緣由上來看,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版本爲例。
首先,先看如下配置,定義了一個經過主鍵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的代碼很是多,接下來主要闡釋和本次案例相關的內容。
在框架初始化階段,主要有如下流程,以下圖所示
在框架初始化階段,有一些組件會被構建,接下來進行逐一作個簡單的介紹:
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中,傳入了這個參數,以下圖所示:
在SqlSource的構建階段中,parameterType參數實際上是被忽略不使用的,這也和官方的描述是一致的,3.2.4以前這個parameterType屬性是被忽略的,而後建立了DynamicSqlSource,這個類主要是用於處理Mybatis動態Sql的類。
在框架初始化階段,須要介紹的內容,在3.2.3版本已經介紹完畢,接下來是當執行getStudentById方法時,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。
經過上圖的代碼能夠得知,parameterType在初始化階段未被使用,而是在SQL執行時,獲取到的,但獲取到的類型是parameterObject對應的類型,這個類是用來記錄mapper方法上對應的參數的。以下圖所示,並不是在Sql配置文件中標註的java.lang.String。
接下來,經過SqlSourceBuilder sqlSourceParser 對sql以及計算獲得的類型進行再次處理,當中流程代碼比較長,主要是在這個過程當中去製做 sql方法的入參 和 java類型的綁定關係,mybatis依賴這個綁定關係使用對應的TypeHandler去進行值的轉換,調用鏈路是SqlSourceParser.parse -> 內部類 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping, 以下圖代碼所示。由於當前的parmeterType爲 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類型,生成對應的statmentHandler,由於咱們目前都是用的預編譯SQL,所以生成的statementHandler就是PrepareStatmentHandler,熟悉JDBC的小夥伴應該立刻能夠猜到這對應的語句是什麼類型了。接下來就會對這句SQL語句進行填充,以下圖代碼所示,會經過PrepareStatmentHandler的parameterize方法對Statment進行參數化,也就是進行填充過程。
在PreparseStatmentHandler進行參數化時,會將參數化的職責交給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會忽略parmeterType,在真正進行sql轉換時,從新根據sql方法入參類型計算合適的TypeHandler處理器,因此本案例中的代碼在3.2.3時運行時正常的。
在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的構造方法。
後續流程與3.2.3一致,由於parameter類型爲java.lang.String,在構建parameterMapping時,使用的類型就是java.lang.String。
由於在框架初始化階段,SqlSource中 parameterMapping, id對應的類型就是java.lang.String,致使在進行Sql語句替換時,獲取到的TypeHandler是StringTypeHandler,以下圖所示:
後面的報錯緣由就比較好理解了,在調用StringTypeHandler的setString方法時,報出了java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String的錯誤。
總結一下這個案例的主要緣由是:
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,有不少重要的信息。
岑凱倫、90後軟件工程師、5年服務端開發經驗 微信公衆號: 程序員小岑成長記。