成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

MyBatis版本升級引發的線上告警回顧及原理分析

開發 開發工具
某天晚上,美團到店事業群某項系統服務正在進行常規需求的上線。因為在內部的Plus系統發布時,提示inf-bom版本需要升級,于是我們就將inf-bom版本從1.3.9.6 升級至1.4.2.1.

 [[336279]]

背景

某天晚上,美團到店事業群某項系統服務正在進行常規需求的上線。因為在內部的Plus系統發布時,提示inf-bom版本需要升級,于是我們就將inf-bom版本從1.3.9.6 升級至1.4.2.1,如下圖1所示:

 

圖1 版本升級

 

圖1 版本升級

不過,當服務上線后,開始陸續出現了一些更新系統交互日志方面的報警,這屬于系統的輔助流程,報警如下方代碼所示。我們發現都是跟MyBatis相關的報警,說明在進行類型轉換的時候,系統產生了強轉錯誤。

  1. 更新開票請求返回日志, id:{#######}, response:{{"code":XXX,"data":{"callType":3,"code":XXX,"msg":"XXXX","shopId":XXXXX,"taxPlateDockType":"XXXXXXX"},"msg":"XXXXX","success":XXXX}} 
  2. nested execption is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='updateTime', mode=IN, javaType=class java.lang.String, 
  3. 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 
  4. 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 

因為報警這一塊代碼,屬于歷史功能,如果失敗并不會影響主流程。但在定位期間,如果頻繁報警的話,就會造成一定的干擾。因此,我們馬上采取了回滾操作,將inf-bom的版本回滾至歷史版本,直至報警消失,然后再進行問題的定位和分析。以下章節就是我們對報警原因的定位及原因詳細分析的介紹,希望這些思路能夠對大家有所啟發和幫助。

報警原因定位

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

第一步,查看了報警的Mapper方法,如下代碼段所示。這個是接收返回參數,根據主鍵id,更新具體響應內容和時間的代碼,入參有3個,類型分別為long、String和LocalDateTime。

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

第二步,我們查看了Mapper方法對應的XML文件,如下代碼段所示,對應的parameterType類型是String,而實際參數的類型包括long、String以及LocalDateTime。

  1. <update id="updateResponse" parameterType="java.lang.String"
  2. UPDATE invoice_log 
  3.   SET response = #{response}, update_time = #{updateTime} 
  4. WHERE id = #{id} 
  5. </update

 

​ 第三步,我們查看了MyBatis上線前后的版本,報警的內容是:MyBatis在處理SQL語句時,發現不能將LocalDateTime轉型為String,這一段邏輯在上線前是可以正常運行的,并且上線的業務邏輯對這段歷史代碼無改動。因此,我們猜測是因為inf-bom的升級,從而導致MyBatis的版本發生了變化,對某些歷史功能不再支持了。MyBatis版本上線前后的變化如下表所示:

 

表1 MyBatis版本升級前后對比

 

表1 MyBatis版本升級前后對比

第四步,我們通過第三步可以得到,在這次inf-bom的版本升級中,MyBatis的版本直接升了兩個大版本,因此我們可以基本將原因猜測為MyBatis升級跨度較大,導致部分歷史功能沒有兼容支持,從而引起線上SQL的更新報錯。

第五步,為了具體驗證第四步的想法,我們通過UT的方式,將MyBatis的版本不斷從3.4.6往下降,直至沒有報錯的位置。最終的定位是:當MyBatis版本為3.2.3時,線上代碼是正常可用的,但只要升一個版本,也就是自3.2.4開始,就開始不兼容目前的用法。不過,我們當時的思路并不是很好,應該從小版本逐個往上升或者使用二分法,可以加速定位版本的效率。

最后,我們定位到了產生報警的根本問題??偟膩碚f,MyBatis版本由inf-bom引入而來,inf-bom從3.2.3 升級到了3.4.6版本,而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并不相同。

  1. public StudentEntity getStudentById(@Param("id"int id); 
  2. <select id="getStudentById" parameterType="java.lang.String" resultType="entity.StudentEntity"
  3. SELECT id,name,age FROM student WHERE id = #{id} 
  4. </select

 

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

在框架初始化階段,主要包括以下流程,如下圖2所示:

 

圖2 框架初始化流程

 

圖2 框架初始化流程

在框架初始化階段,有一些組件會被構建,逐一做個簡單的介紹:

  • SqlSession:作為MyBatis工作的主要頂層API,表示和數據庫交互的會話,完成必要的數據庫增刪改查功能。
  • 數據庫增刪改查功能:負責根據用戶傳遞的parameterObject,動態地生成SQL語句,將信息封裝到BoundSql對象中,并返回。
  • Configuration:MyBatis所有的配置信息都維持在Configuration對象之中。

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

在構建Configuration的過程中,會涉及到構建對應每一條SQL語句對應的MappedStatement,parameterTypeClass就是根據我們在XML配置中寫的parameterType轉換而來,值為java.lang.String,在構建SqlSource時,傳入這個參數。如下圖3所示:

 

圖3 SqlSource依賴參數

 

圖3 SqlSource依賴參數

在SqlSource的構建中,parameterType參數其實是被忽略不用的,并沒有繼續往下傳遞,這跟官方的描述是一致的。因為3.2.4之前這個parameterType屬性被忽略了,然后就創建了DynamicSqlSource,這個類主要是用于處理MyBatis動態SQL的類。如下圖4所示:

 

圖4 SqlSource構建

 

圖4 SqlSource構建

在框架初始化的階段,需要介紹的內容,在3.2.3版本已經介紹完畢。當執行getStudentById方法時,MyBatis的流程如下圖5所示。因受限于圖片長度,我們對布局進行了一些調整:

 

圖5 運行流程

 

圖5 運行流程

在具體執行階段,也涉及到一些組件,我們需要做簡單的了解:

  • 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,如下圖6所示:

 

圖6 獲取BoundSql

 

圖6 獲取BoundSql

通過圖6的代碼,我們可以得知,parameterType在初始化階段未被使用,而是在SQL執行時獲取到的,但獲取到的類型是parameterObject對應的類型,這個類是用來記錄Mapper方法上對應的參數。如下圖7所示,它并非在SQL配置文件中標注的java.lang.String。

 

圖7 parameterObject類型

 

圖7 parameterObject類型

然后我們通過SqlSourceBuilder的parse方法對SQL以及獲取到的類型進行再次處理,其中的流程代碼比較長。在這個過程中,我們主要去構建SQL的參數和Java類型的綁定關系,MyBatis依賴這個綁定關系,使用對應的TypeHandler去進行值的轉換。

調用鏈路是SqlSourceParser.parse -> 內部類 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping,如下圖8中的代碼所示。因為當前的parameterType為MapperMethod$ParamMap,經過了多個if判斷,判定當前property id的propertyType為Object.class類型。接下來,構建SQL的參數和Java類型的綁定關系ParameterMapping,再進行返回。

 

圖8 buildParameterMapping過程

 

圖8 buildParameterMapping過程

構建完成的ParameterMapping的結構如下圖9中的代碼所示,參數id對應的javaType類型為java.lang.Object,對應的TypeHander處理器為UnknownTypeHandler,也就是未找到合適的TypeHandler的兜底選項。

 

圖9 ParameterMapping結構

 

圖9 ParameterMapping結構

接下來,流程就會流轉到Executor,在org.apache.ibatis.executor.SimpleExecutor#doQuery進行查詢時,會根據當前的SQL類型,生成對應的StatementHandler。因為我們目前都是用的預編譯SQL,因此生成的statementHandler就是PreparedStatementHandler,熟悉JDBC的小伙伴應該馬上可以猜到對應的語句是什么類型了。然后,我們對這句SQL語句進行填充,如下圖10中的代碼所示。我們會通過PreparedStatementHandler的parameterize方法對Statement進行參數化,也就是進行填充。

 

圖10 PrepareStatement處理過程

 

圖10 PrepareStatement處理過程

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

 

 

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

 

圖12 獲取可用TypeHandler

 

圖12 獲取可用TypeHandler

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

 

圖13 獲取IntegerHandler

 

圖13 獲取IntegerHandler

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

 

圖14 IntegerHander值替換

 

圖14 IntegerHander值替換

后續就是執行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中發生了什么變化。如圖15所示,與3.2.3不同,3.2.4首先判斷了是否為動態SQL,在非動態SQL情況下,才會將parameterType java.lang.String作為參數,傳入SqlSource的構造方法。

 

圖15 生成SqlSource

 

圖15 生成SqlSource

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

 

圖16 構建ParameterMapping與3.2.3版本的差異

 

圖16 構建ParameterMapping與3.2.3版本的差異

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

 

圖17  整數類型的參數獲取到了StringTypeHandler

 

圖17 整數類型的參數獲取到了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不匹配時,進而會出現類型轉換報錯。

這一段排查的經歷,對自己后續編寫代碼及在系統上線時也有一些啟發,主要包括以下幾個方面:

在inf-bom升級時,需要線下進行全面回歸,要避免框架存在不兼容的用法,不然的話,就容易導致線上錯誤。

開發同學可以檢查自己系統內的MyBatis版本,如果是3.2.4以下,需要全面檢查下現在的Mapper文件里對于parameterType的使用和Mapper方法中實際的參數類型是否一致,避免升級到3.2.4及以上版本時發生轉型報錯。如果有不匹配的情況存在,需要進行修正或者不使用parameterType,讓MyBatis在運行SQL時自動計算對應的類型。

可以考慮使用MyBatis-Generator來自動生成XML和Mapper文件,畢竟是專業團隊在維護,穩定性相對來說會更好一些,同時能夠避免手動修改XML文件帶來的誤操作。

可以主動關注強依賴的一些開源框架的Release Log,不要錯過了重要的信息。

作者簡介

凱倫,2016年校招加入美團,后端開發工程師。

 

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2021-10-29 19:00:30

監控系統數據庫

2020-05-07 11:00:24

Go亂碼框架

2021-08-09 11:15:28

MybatisJavaSpring

2009-02-25 16:02:13

2018-01-16 08:50:49

容器技術Kubernetes

2023-02-16 08:55:13

2009-08-13 09:46:49

C#歷史C# 4.0新特性

2022-12-08 09:34:26

開發操作

2009-01-18 16:33:09

pureXMLDB2 pureXMLXML

2012-11-12 00:13:12

回顧數據中心云計算

2023-04-26 18:36:13

2024-01-16 10:25:07

2023-04-28 08:30:56

MyBatis架構API

2021-03-31 08:02:34

Prometheus 監控運維

2009-01-20 16:22:15

2021-06-04 15:58:53

CPU排查OOM

2009-08-05 17:04:14

.NET委托

2020-02-11 19:19:33

安全態勢亞信安全網絡安全

2020-12-17 08:02:42

MyBatis插件框架

2020-04-02 07:31:53

RPC超時服務端
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 五月激情综合网 | 亚洲一区二区电影在线观看 | 亚洲欧洲视频 | 91精品国产色综合久久不卡98 | 亚洲精品成人av | 久久高清 | 亚洲成人激情在线观看 | 黄网址在线观看 | 日韩精品一区二区三区视频播放 | 在线免费av电影 | 日日天天 | jvid精品资源在线观看 | 999久久久国产精品 欧美成人h版在线观看 | 91久操视频 | 在线一区视频 | 成人av在线播放 | 国产一区 | 欧美日韩视频在线播放 | 欧美啊v在线观看 | 午夜大片 | 国产精品久久久久久久久久了 | 日韩毛片 | 久久久久国产一级毛片 | 欧美一区二区三区大片 | 精品一区久久 | 一区二区三区韩国 | 久久久日韩精品一区二区三区 | 国产综合久久久 | 成人精品一区二区三区中文字幕 | 成人av影院 | 欧美一级三级 | 日韩av在线中文字幕 | 色综合视频 | 免费av观看| 99久久精品免费看国产免费软件 | 国产日韩一区二区三免费高清 | 成人精品一区亚洲午夜久久久 | 久久亚洲国产 | 欧美jizzhd精品欧美巨大免费 | 狠狠艹| h片在线免费观看 |