淺談Mybatis版本升級(jí)踩坑及背后原理分析
1、背景
某一天的晚上,系統(tǒng)服務(wù)正在進(jìn)行常規(guī)需求的上線,因?yàn)榘l(fā)布時(shí),提示統(tǒng)一的pom版本需要升級(jí),于是從 1.3.9.6 升級(jí)至 1.4.2.1。
當(dāng)服務(wù)開始上線后,開始陸續(xù)出現(xiàn)了一些更新系統(tǒng)交互日志方面的報(bào)警,屬于系統(tǒng)輔助流程,報(bào)警下圖所示, 具體系統(tǒng)數(shù)據(jù)已脫敏,內(nèi)容是Mybatis相關(guān)的報(bào)警,在進(jìn)行類型轉(zhuǎn)換的時(shí)候,產(chǎn)生了強(qiáng)轉(zhuǎn)錯(cuò)誤。
更新開票請(qǐng)求返回日志, 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
報(bào)警的一塊代碼,屬于歷史功能,失敗并不會(huì)影響主流程,但在定位期間,會(huì)頻繁報(bào)警,造成一定的干擾,因此當(dāng)時(shí)首先采取回滾操作,將統(tǒng)一的pom版本回滾至歷史版本,報(bào)警消失,再進(jìn)行問題的定位和分析。
以下章節(jié)是對(duì)報(bào)警原因的定位及原因詳細(xì)分析的介紹。
2、報(bào)警原因定位
首先是具體的報(bào)警原因:
由于mybatis版本由inf-bom引入而來,在inf-bom升級(jí)后,由3.2.3 升級(jí)至了 3.4.6版本,而Mybatis自3.2.4開始就不支持目前系統(tǒng)內(nèi)的SQL Mapper的用法,因此上線后,線上出現(xiàn)頻繁報(bào)警。接下來是定位的過程。
回滾完畢后,開始具體分析報(bào)警產(chǎn)生的主要原因,進(jìn)行了以下幾步的排查。
1.查看了報(bào)警的Mapper方法,如下代碼所示, 這個(gè)是接收返回參數(shù),根據(jù)主鍵id,更新具體響應(yīng)內(nèi)容和時(shí)間的代碼,入?yún)⒂?個(gè),類型分別為long, String 和 LocalDateTime
int updateResponse(@Param("id")long id, @Param("response")String response, @Param("updateTime")LocalDateTime updateTime);
2.查看了Mapper方法對(duì)應(yīng)的XML文件,如下代碼,對(duì)應(yīng)的parameterType類型是String,而實(shí)際參數(shù)的類型有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上線前后的版本,因?yàn)閳?bào)警的內(nèi)容是Mybatis處理sql語句時(shí),發(fā)現(xiàn)不能將LocalDateTime轉(zhuǎn)型為String,這一段邏輯在上線前是ok的,上線的業(yè)務(wù)邏輯對(duì)這段歷史代碼無改動(dòng),因此猜測(cè)是統(tǒng)一pom的升級(jí),導(dǎo)致Mybatis的版本發(fā)生了變化,某些歷史功能不支持了。 mybatis版本上線前后的變化,1.3.9.6對(duì)應(yīng)的版本是3.2.3,1.4.2.1對(duì)應(yīng)的版本是3.4.6。
4.通過第3步可以得到,在這次inf-bom的版本升級(jí)中,mybatis3的版本直接升了兩個(gè)大版本,因此可以基本將原因猜測(cè)為 Mybatis升級(jí)跨度大,導(dǎo)致部分歷史功能沒有兼容支持,引起的線上sql更新報(bào)錯(cuò)。
5.為了具體驗(yàn)證第4步的想法,通過UT的方式,通過將Mybatis的版本不斷從3.4.6往下降,直至沒有報(bào)錯(cuò)位置,最終定位是Mybatis版本為3.2.3時(shí),線上代碼是正常可用的,只要升一個(gè)版本也就是自3.2.4開始,就開始不兼容目前的用法。(這個(gè)當(dāng)時(shí)思路不是很好,應(yīng)該從小版本逐個(gè)往上升,可以去加速定位版本的效率)
最后定位報(bào)警原因,由于mybatis版本由統(tǒng)一pom引入而來,在統(tǒng)一pom升級(jí)后,由3.2.3 升級(jí)至了 3.4.6版本,而Mybatis自3.2.4開始就不支持目前系統(tǒng)內(nèi)的SQL Mapper的用法,因此上線后,線上出現(xiàn)頻繁報(bào)警。
報(bào)警原因已定位,但為什么版本升級(jí)后就不兼容歷史的用法,并且具體不兼容的是哪一塊內(nèi)容,背后的原理又是什么,請(qǐng)看接下來章節(jié)的詳細(xì)分析。
3、詳細(xì)分析
3.1 Mybatis 升級(jí)3.2.4版本的官方Release公告
首先從報(bào)錯(cuò)的原因上來看,Caused by: java.lang.ClassCastException: java.lang.LocalDateTime cannot be cast to java.lang.String ,是Mybatis在構(gòu)建sql語句時(shí),發(fā)現(xiàn)時(shí)間字段 類型為LocalDateTime 不能強(qiáng)制轉(zhuǎn)為String類型。 這個(gè)SQL XML的配置在3.2.3的版本是正??梢杂茫敲词紫仁菑腗ybatis 的 release log上查看3.2.4版本 發(fā)生了什么變化。
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.
從官網(wǎng)的Release Log可以看出,Mybatis在3.2.4以前的版本,是忽略XML中的parameterType這個(gè)屬性,并且使用真實(shí)的變量類型進(jìn)行值的處理,在3.2.4及以后的版本中,這個(gè)屬性會(huì)被啟用,因此如果出現(xiàn)類型不匹配的話,就會(huì)出現(xiàn)轉(zhuǎn)型失敗的報(bào)錯(cuò),也提示我們開發(fā)者在升級(jí)到這個(gè)版本及以上時(shí),需要檢查系統(tǒng)內(nèi)的XML配置,使類型相匹配,或者不設(shè)置該屬性,讓Mybatis自行進(jìn)行計(jì)算。
從以上內(nèi)容,可以了解到,在版本升級(jí)后,mybatis在構(gòu)建sql語句,獲取字段值的時(shí)候邏輯發(fā)生了變化,那么接下來通過一個(gè)普通的示例,了解mybatis在獲取字段值這一塊的具體代碼流程是怎樣的,以3.2.3版本為例。
3.2 以版本3.2.3為例,mybatis構(gòu)建SQL語句過程的原理分析
首先,先看以下配置,定義了一個(gè)通過主鍵id獲取學(xué)生信息的方法,仿造系統(tǒng)內(nèi)的歷史代碼,也將parameterType定義為 java.lang.String 和 方法對(duì)應(yīng)的參數(shù) 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框架要做的事情就是在運(yùn)行g(shù)etStudentById(2)的時(shí)候,將 #{id}進(jìn)行替換,使SQL語句變成 SELECT id,name,age FROM student WHERE id = 2 。Mybatis要將SQL語句完整替換成帶參數(shù)值的版本,需要經(jīng)歷框架初始化以及實(shí)際運(yùn)行時(shí)動(dòng)態(tài)替換兩個(gè)部分。因?yàn)镸ybatis的代碼非常多,接下來主要闡釋和本次案例相關(guān)的內(nèi)容。
在框架初始化階段,主要有以下流程,如下圖所示
在框架初始化階段,有一些組件會(huì)被構(gòu)建,接下來進(jìn)行逐一做個(gè)簡單的介紹:
- SqlSession 作為MyBatis工作的主要頂層API,表示和數(shù)據(jù)庫交互的會(huì)話,完成必要數(shù)據(jù)庫增刪改查功能。
- SqlSource 負(fù)責(zé)根據(jù)用戶傳遞的parameterObject,動(dòng)態(tài)地生成SQL語句,將信息封裝到BoundSql對(duì)象中,并返回。
- Configuration MyBatis所有的配置信息都維持在Configuration對(duì)象之中。
接下來主要關(guān)注SqlSource,這個(gè)類會(huì)負(fù)責(zé)在負(fù)責(zé)生成SQL語句,也是本次案例中,3.2.3和3.2.4差異比較大的地方。接下來會(huì)一些源碼部分的介紹。
在構(gòu)建Configuration的過程中,會(huì)涉及到構(gòu)建對(duì)應(yīng)每一條sql語句對(duì)應(yīng)的MappedStatemnt,在parmeterTypeClass就是根據(jù)我們?cè)趚ml配置中寫的parmeterType轉(zhuǎn)換而來,值為java.lang.String,在接下來構(gòu)建SqlSource中,傳入了這個(gè)參數(shù),如下圖所示:
在SqlSource的構(gòu)建階段中,parameterType參數(shù)其實(shí)是被忽略不使用的,這也和官方的描述是一致的,3.2.4之前這個(gè)parameterType屬性是被忽略的,然后創(chuàng)建了DynamicSqlSource,這個(gè)類主要是用于處理Mybatis動(dòng)態(tài)Sql的類。
在框架初始化階段,需要介紹的內(nèi)容,在3.2.3版本已經(jīng)介紹完畢,接下來是當(dāng)執(zhí)行g(shù)etStudentById方法時(shí),Mybatis的流程,如下圖所示,受限于圖片長度,進(jìn)行了布局的調(diào)整:
在具體執(zhí)行階段,也有一些組件,我們需要做了解
SqlSession 作為MyBatis工作的主要頂層API,表示和數(shù)據(jù)庫交互的會(huì)話,完成必要數(shù)據(jù)庫增刪改查功能
Executor MyBatis執(zhí)行器,是MyBatis 調(diào)度的核心,負(fù)責(zé)SQL語句的生成和查詢緩存的維護(hù)
BoundSql 表示動(dòng)態(tài)生成的SQL語句以及相應(yīng)的參數(shù)信息
StatementHandler 封裝了JDBC Statement操作,負(fù)責(zé)對(duì)JDBC statement 的操作,如設(shè)置參數(shù)、將Statement結(jié)果集轉(zhuǎn)換成List集合。
ParameterHandler 負(fù)責(zé)對(duì)用戶傳遞的參數(shù)轉(zhuǎn)換成JDBC Statement 所需要的參數(shù)
TypeHandler 負(fù)責(zé)java數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的映射和轉(zhuǎn)換
接下來主要關(guān)注在獲取BoundSql以及參數(shù)化語句的流程,也是本次案例中,3.2.3和3.2.4差異比較大的地方。接下來會(huì)一些源碼部分的介紹。
在進(jìn)入Executor的query方法后,會(huì)首先通過對(duì)應(yīng)的MappedStatement獲取BoundSql,用來幫助我們動(dòng)態(tài)生成SQL語句,里面綁定了對(duì)應(yīng)的SQL以及參數(shù)映射關(guān)系,在構(gòu)建框架階段,我們使用的SqlSource是DynamicSqlSource,通過這個(gè)類來生成獲取BoundSql。
通過上圖的代碼可以得知,parameterType在初始化階段未被使用,而是在SQL執(zhí)行時(shí),獲取到的,但獲取到的類型是parameterObject對(duì)應(yīng)的類型,這個(gè)類是用來記錄mapper方法上對(duì)應(yīng)的參數(shù)的。如下圖所示,并非在Sql配置文件中標(biāo)注的java.lang.String。
接下來,通過SqlSourceBuilder sqlSourceParser 對(duì)sql以及計(jì)算得到的類型進(jìn)行再次處理,當(dāng)中流程代碼比較長,主要是在這個(gè)過程中去制作 sql方法的入?yún)?和 java類型的綁定關(guān)系,mybatis依賴這個(gè)綁定關(guān)系使用對(duì)應(yīng)的TypeHandler去進(jìn)行值的轉(zhuǎn)換,調(diào)用鏈路是SqlSourceParser.parse -> 內(nèi)部類 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping, 如下圖代碼所示。因?yàn)楫?dāng)前的parmeterType為 MapperMethod$ParamMap,進(jìn)過了多個(gè)if判斷,判定當(dāng)前property id 的 propertyType 為Object.class類型,接下來就是制作 sql方法的入?yún)?和 java類型的綁定關(guān)系 parameterMapping,并進(jìn)行了返回。
制作完成的ParameterMapping的結(jié)構(gòu)如下圖代碼所示,參數(shù)id對(duì)應(yīng)的javaType類型為 java.lang.Object,對(duì)應(yīng)的TypeHander處理器為UnknownTypeHandler,也就是未找到合適的TypeHandler的兜底選項(xiàng)。
接下來流程就會(huì)流轉(zhuǎn)到Executor, org.apache.ibatis.executor.SimpleExecutor#doQuery進(jìn)行查詢時(shí),會(huì)根據(jù)當(dāng)前的SQL類型,生成對(duì)應(yīng)的statmentHandler,因?yàn)槲覀兡壳岸际怯玫念A(yù)編譯SQL,因此生成的statementHandler就是PrepareStatmentHandler,熟悉JDBC的小伙伴應(yīng)該馬上可以猜到這對(duì)應(yīng)的語句是什么類型了。接下來就會(huì)對(duì)這句SQL語句進(jìn)行填充,如下圖代碼所示,會(huì)通過PrepareStatmentHandler的parameterize方法對(duì)Statment進(jìn)行參數(shù)化,也就是進(jìn)行填充過程。
在PreparseStatmentHandler進(jìn)行參數(shù)化時(shí),會(huì)將參數(shù)化的職責(zé)交給DefaultParameterHandler進(jìn)行,如下圖代碼所示,主要關(guān)注紅線部分,首先會(huì)獲取parameterMapping對(duì)應(yīng)的TypeHander,如上章節(jié)所示,獲取到的是UnknownTypeHandler,然后會(huì)通過setParameter方法,將參數(shù)id替換成對(duì)應(yīng)的值。
在typehandler的流程里,首先會(huì)進(jìn)入BaseTypeHandler,然后在具體設(shè)置時(shí),進(jìn)入子類的方法,在UnknownTypeHandler,首先會(huì)再次對(duì)parameter進(jìn)行解析,判斷最正確的TypeHandler類型,如下圖代碼所示:
在resolveTypeHandler方法中,因?yàn)橐阎獏?shù)值的類型,通過Integer這個(gè)class在typeHandlerRegistry中尋找對(duì)應(yīng)的TypeHandler,TypeHandlerRegistry是Mybatis啟動(dòng)時(shí)內(nèi)置好的,java對(duì)象類型和TypeHandler的映射關(guān)系,有興趣的可以進(jìn)這個(gè)類詳細(xì)看下,在本案例中,會(huì)直接獲取到IntegerHandler,如下圖代碼所示:
在獲取到IntegerHandler后,就可以使用IntegerTypeHandler的setInt方法,對(duì)SQL語句中的參數(shù)進(jìn)行替換,如下圖代碼所示,sql語句被成功替換。
后續(xù)就是執(zhí)行SQL并處理返回結(jié)果,不在本文的討論范圍內(nèi),從上文的分析中,我們可以了解到,在3.2.3及以下版本,Mybatis會(huì)忽略parmeterType,在真正進(jìn)行sql轉(zhuǎn)換時(shí),重新根據(jù)sql方法入?yún)㈩愋陀?jì)算合適的TypeHandler處理器,所以本案例中的代碼在3.2.3時(shí)運(yùn)行時(shí)正常的。
3.3 以版本3.2.4為例,相比版本3.2.3,mybatis構(gòu)建SQL語句過程的變化分析
在3.2章節(jié)中,得知mybatis是在運(yùn)行sql階段重新計(jì)算參數(shù)對(duì)應(yīng)的TypeHandler進(jìn)行sql參數(shù)替換,那么在版本3.2.4中,mybatis做了什么改動(dòng),導(dǎo)致了原有的使用方式不可用了呢。從官方的release log來看,版本3.2.4做了這樣一個(gè)改動(dòng)。
This version builds the binding information during startup and the "parameterType" attribute is used
意思是說 parameterType會(huì)在框架運(yùn)行階段就被使用到,從這個(gè)中,我們將分析的重點(diǎn)放在構(gòu)建階段,同時(shí)負(fù)責(zé)處理綁定關(guān)系的BoundSql由配置階段的SqlSource生成,因此主要查看SqlSource的構(gòu)建,3.2.4發(fā)生了什么變化,如下圖所示。與3.2.3不同,3.2.4首先判斷了是否為動(dòng)態(tài)SQL,在非動(dòng)態(tài)SQL情況下,將parameterType java.lang.String作為參數(shù),傳入了SqlSource的構(gòu)造方法。
后續(xù)流程與3.2.3一致,因?yàn)閜arameter類型為java.lang.String,在構(gòu)建parameterMapping時(shí),使用的類型就是java.lang.String。
因?yàn)樵诳蚣艹跏蓟A段,SqlSource中 parameterMapping, id對(duì)應(yīng)的類型就是java.lang.String,導(dǎo)致在進(jìn)行Sql語句替換時(shí),獲取到的TypeHandler是StringTypeHandler,如下圖所示:
后面的報(bào)錯(cuò)原因就比較好理解了,在調(diào)用StringTypeHandler的setString方法時(shí),報(bào)出了java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String的錯(cuò)誤。
4、總結(jié)
總結(jié)一下這個(gè)案例的主要原因是:
mybatis 3.2.3版本 兼容parameterType和實(shí)際參數(shù)類型不匹配,運(yùn)行時(shí)動(dòng)態(tài)計(jì)算值處理器類型,在大版本升級(jí)2個(gè)版本號(hào)后,parameterType開始生效,以parameterType作為參數(shù)的實(shí)際類型進(jìn)行TypeHandler的獲取計(jì)算,導(dǎo)致類型不匹配時(shí),強(qiáng)轉(zhuǎn)報(bào)錯(cuò)。
帶給我自己的在后續(xù)編寫編寫代碼及系統(tǒng)上線方面的啟示是:
1.在統(tǒng)一pom升級(jí)時(shí),需要線下進(jìn)行全面回歸,避免框架存在不兼容的用法,導(dǎo)致線上錯(cuò)誤。
2.開發(fā)同學(xué)可以檢查自己系統(tǒng)內(nèi)的mybatis版本,如果是3.2.4以下,需要全面檢查下現(xiàn)在的mapper文件里 對(duì)于parameterType的使用 和實(shí)際的參數(shù)類型是否一致,避免升級(jí)到3.2.4及以上版本時(shí)發(fā)生兼容報(bào)錯(cuò),如果有不匹配的情況存在,需要進(jìn)行修正 或者 不使用parameterType,讓Mybatis在運(yùn)行SQL時(shí)自動(dòng)計(jì)算對(duì)應(yīng)的類型,
3.可以考慮使用mybatis-generator來自動(dòng)生成xml和mapper文件,有專業(yè)團(tuán)隊(duì)維護(hù),相對(duì)來說穩(wěn)定性更好,也避免自己手動(dòng)修改xml文件容易帶來誤操作。
4.可以主動(dòng)關(guān)注強(qiáng)依賴的一些開源框架的Release log,有很多重要的信息。
到此這篇關(guān)于淺談Mybatis版本升級(jí)踩坑及背后原理分析的文章就介紹到這了,更多相關(guān)Mybatis版本升級(jí)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Java的內(nèi)置異常以及創(chuàng)建自定義異常子類的方法
這篇文章主要介紹了詳解Java的內(nèi)置異常以及創(chuàng)建自定義異常子類的方法,是Java入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09java正則表達(dá)式匹配網(wǎng)頁所有網(wǎng)址和鏈接文字的示例
這篇文章主要介紹了java正則表達(dá)式匹配網(wǎng)頁所有網(wǎng)址和鏈接文字java正則表達(dá)式匹配,需要的朋友可以參考下2014-03-03詳細(xì)分析Java并發(fā)集合ArrayBlockingQueue的用法
這篇文章主要介紹了詳細(xì)分析Java并發(fā)集合ArrayBlockingQueue的用法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04java動(dòng)態(tài)代理(jdk與cglib)詳細(xì)解析
靜態(tài)代理:由程序員創(chuàng)建或特定工具自動(dòng)生成源代碼,再對(duì)其編譯。在程序運(yùn)行前,代理類的.class文件就已經(jīng)存在了2013-09-09關(guān)于SHA算法原理與常用實(shí)現(xiàn)方式
這篇文章主要介紹了關(guān)于SHA算法原理與常用實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08SpringBoot接口或方法進(jìn)行失敗重試的實(shí)現(xiàn)方式
為了防止網(wǎng)絡(luò)抖動(dòng),影響我們核心接口或方法的成功率,通常我們會(huì)對(duì)核心方法進(jìn)行失敗重試,如果我們自己通過for循環(huán)實(shí)現(xiàn),會(huì)使代碼顯得比較臃腫,所以本文給大家介紹了SpringBoot接口或方法進(jìn)行失敗重試的實(shí)現(xiàn)方式,需要的朋友可以參考下2024-07-07SWT(JFace)體驗(yàn)之打開多個(gè)Form
SWT(JFace)體驗(yàn)之打開多個(gè)Form的實(shí)現(xiàn)代碼。2009-06-06JFileChooser實(shí)現(xiàn)對(duì)選定文件夾內(nèi)圖片自動(dòng)播放和暫停播放實(shí)例代碼
這篇文章主要介紹了JFileChooser實(shí)現(xiàn)對(duì)選定文件夾內(nèi)圖片自動(dòng)播放和暫停播放實(shí)例代碼,需要的朋友可以參考下2017-04-04