MyBatis版本升级引发的线上告警回顾及原理分析

MyBatis版本升级引发的线上告警回顾及原理分析

背景

某天晚上,美团到店事业群某项系统服务正在进行常规需求的上线。因为在内部的Plus系统发布时,提示inf-bom版本需要升级,于是我们就将inf-bom版本从1.3.9.6 升级至1.4.2.1,如下图1所示:
MyBatis版本升级引发的线上告警回顾及原理分析
图1 版本升级
不过,当服务上线后,x . ` P ` t N开始陆续出现了一些更新系统交互日志方面的报警,这属于系统的辅助流程2 + $ e a T O P,报J } I ~ { S u m警如下方代码G y 2 z S S 0所示。我们发现都是跟MyBatis相关的报警,说明在进行类型转换的时候,系统产生了强转错误。


1.  更新开票请求返回日志, id:{#######}, response:{{"code":XXX,"data":{"call! % W j $ T _ !Type":3,"code":XXX,"msg":z & `  K P = 1 N"XXXX","shopId":XXXXX,"taxPlj / _ , L ]ateDockType":"s w 7 g 7 K +XXXXXXX"},"msg":"XXXXX","success":XXXX}}
2.  nested execption is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: P: ] : $arameterMapping{property='updateTime', mode=IN, javaType=class java.lang.String,
3.  jdbcTyp=null,resultMapId='null',jdbcTypeName='null b n ! f @l',expressionU : B - C H x . y='null'}.Cause org.apache.ibatis.type.TypeException,Error setting non nuY n  d nll 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 tot L 8 { S } 0 java.lang.Strin; _ Lg

因为报警这一块代码,属于历史功能,如果失败并不会影响主流程。但在定位期间,如果频繁报警的话,就会造成一定的干扰。因此,我们马上采取了回滚操作,将inf-bom的版本回滚至历史版本,直至报警消失,然后再进行问题的定位和分析。以下章节就是我们对c * v 3 O # S K报警原因的定位及原因详细分析的介绍,希望这些思路能够对大家有所启发和帮助。

报警原因定位

在回滚完毕后,我们开始具体分析报警产生的主要原因,于是进行了以下几步的排查。
第一步,查看了报警的Mapper- 2 e T 5 M 5 V 9方法,如下代码段所示。这个是接收返回参数,根据主键id,更新具体响应内容和时间的代码,入参有3个,类m 6 h型分别为long、String和LocalDateTime。


1.  int updateResponse(Z N A ~ ]@Param("id")long id, @Param("resr e p f K =ponse")StrinS } ; 7 A i h =g response, @Param("updateTime")LocalDateTime updateTime); 

第二步,我们查看了Mapper方法对应的XML文件,如下代码段所示,对应的parameterType类型是String,而实际参] x !数的类型包括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语句时,发现不能将LocalDate1 9 S $ ] :Time转型为Strinr ] }g,这一段逻辑在上线前是可以正常运行的,并且上线的业务逻辑对这段历史代码无改动。因此,我们t O f / 4猜测是因为inf-bom的升级,从而导致MyBatis的版本发生了变化,对某些历史功能不再支持了。MyBatis版本上线前后的变化如下表所示:
MyBatis版本升级引发的线上告警回顾及原理分析
表1 MyBatis版本升级前后对比

第四步,我们通过第三步可以得到,在这次inf-bom的版本升级H C | + y B U r中,MyBatis的版本直接升了两个C l 大版本,因此我们可以基本将原因猜测为MyBatis升级跨度较大,导致部分历史功能没有, # l 4 M 兼容支持,从而引起线上SQL的更新报错。
第五步,为了) V : ~ Q Q ~ 具体验证第四步的想法,我们通过UT的方式,将MyBatis的版本不断从3.4.6往下降,直至没有报错的位置。最终的定位是:当MyBatis版本为3.2.3时,线上代码是正常可用的,但只要升一个版本,也就是自3.2.4开始,就开始不兼容目前的用法。不过,我们当时的思路并不是很好,应该从小版` z g e R ` 0本逐个往上升或者使用二分法,可以加速定位版本的效率。
最后,我} w C k I们定位到了产生报警的根本问题。总的来说,MyBatis版本由inf-bom引入而来,inf-bom从3.2.3 升级到了3.0 + S ) g ;4.6版本,而MyBatis自3.2.4开始就不支持目前系统内的SQL Mapper的用法,因此在升级后,线上就出现了频繁报警的问题。
问题已经定位,但是还有很多事情我们需要弄清楚。为什么版本升级后就不兼容历史的用法?具体G ? + j M U : I 7是哪一块内容不兼容?背后的原理又是什z z O 2么?下文,我们会详细进行分析: c ? ;

详细分s y E g W {

MyBatis升级3.2.4版本的g v [ j官方Release公告

首先,从报错的原因上T % / k来看,请注意这句话:“Caused by: java.lang.ClassCastException: java.lang.LocV z 2 e = 5alDatJ # ! ) v | , heTime cannot be caV d & M tst to java.lang.S] S btring.”MyBatis在构建SQL语句时,发现时间字段类型LocalDateTime不能强 h - / X ( T [ |制转为String类d U ? # h Z S型。而这个SQL对应的XML配置在3.2.3的版本是可以正常使用的- u Z / m f . F /,那么我们先从MyBatis的RelF E G Y nease Log上查看3G P q | V [ I 4 $.2.4版本到底发u h v W `生了什么变化。

An special remark about this feature. PreviouI l M i + N ! =s versions ignored the “parameterType” attribute and used the actual parameter to calculate bindings. This version builds the binding information during startupk C ? U and thp l W q 3 o c / ne “parameterType” attribute is used if present (though itO a a p V Y + G ( is still optional), so in case you had a wrong value for it you will havK 8 @ S ge to change it.

从官网的Release Log可以看到,MyBatis在3.2.4以前的版本,会忽略XML中的paC ] ] : * 8rameterType这个属性,并且使用真实的变量类@ ; ` #型进行值的处理。但在3.2.4及以后的版本中,这个属| ] Q F W性就被启用了,如果出现类型不匹配的话,就会出现转型失I v 8 , z K N败的报错。这也提示我们开发者,在升级版本时,需要检查L 1 c )系统内的XML配置,使类型进行匹配,或者不设置该属性,让MyBatis自行进行计算。
根据以上内容,我们可以了解到,在版本升级后,MyBa^ c 0 gtis在构建SQL语句,在获取字段值时的逻辑发生了变化。接下来我们将通过一个简单的示例,来了解一| p j下MyBatis在获取字段值这一块的具体代码流程是怎样的,以3.2.3版本为例。
以版本3.2.3为例,MyB9 U # 1 v F I [atx J Yis构建SQL语句过程的原理分析
我们看一下配置,首先定义* [ ~一个通过主键id获取学生信息的方法,仿造系统内的历史代码,我们将parameterType定义为java.lang.String,这和方法对应的参数int并不相同。

1.  public StudentEntity getStudentById(@Param("id") int id);
2.  <select id="getStudentById" parameterTy? 6 @ O E H q Vpe="java.lang.String" resultW y k q k O = vType="enn s I v , y z Atity.StudentEntity"&gZ = .t;
3.  SELECT id,name,age! N G f H ; M b FROM student WHERE id = #{id}
4.  </select> 

MyBatis框架要做的事情,就是在运行getStudentById(2)的时候,将 #{id}进行替换,使SQL语句变成SELECT id,name,age FROM student WHER] 8 K # ! b q vE id = 2。MyBatis要将SQL语句完整替换成带参数值的版本,需要经历框架初始化以及实际运行时动态替换这两个部分。因为MyBatis的代码非常多,接下来我们主要阐释和本次案例相关的内容。
在框架初始化阶段,主要包C p _ U X / ]括以下流程,如下图2所示:
MyBatis版本升级引发的线上告警回顾及原理分析
图2 框架初始化流程

在框架初E # 0 u D始化阶段,有一些组件会被构建,逐一做个简单的介绍:
• SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
• 数据库增删改查功能:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
• Configuration:MyBatis所有的配置信息都维持在I j + ^Conf2 $ n ? # &iguration对象之中。
接下来,我们s k ! @ 0 ] g %主要关注SqlSource,这个类会负责生成SQL语句,这也是本次案例中,3.2.3和3.2.4差异比较大的一个地方。下面,我们会介绍一些源码。
在构建Configuration的过程中,会涉及到构建对应每一条SQL语句对应的MappedStatement,parameterTypeClass就是根据我们在XML配置中写的parameterType转换而来,值为java.lang.String,在构z $ | L建SqlSource时,传入这个参数。如下图3所示:
MyBatis版本升级引发的线上告警回顾及原理分析
图3 SqlSource依赖参数

在SqlSource的构建中,parameterType参数其I ]# a 3 / / ^是被忽略不用的,并没有继续往下传递,这跟官方的描述是一致的。因为3.2.4之前这个parameterType属性被忽略了,然后就创建了DynamicSqlSource,这个类主要是用于处理MyBatis动态SQL的类。如下A a , B R ;图4所示:
MyBatis版本升级引发的线上告警回顾及原理分析
图4 SqlSource构建

在框架初始化的阶段,需要介绍的内容,在3.2.3版本已经介绍完毕。当执行getStudentById方法时,MyBatis的流程如下图5所示。因受限k ; S N L $于图片长度,6 k | e我们对布局进行了一些调整:
MyBatis版本升级引发的线上告警回顾及原理分析
图5 运行流程

在具体R ` h ; . l B执行阶段,也涉及到一些组件,我们需要做简单的了W , 7 - `解:

• SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功a : Z k { )能。
• Executor:MyBatis执行器,这是MyBatis调度的核心,负责SQL语句的生成和查询缓存的维护。
• BoundSql:表示动态生成的SQL语句以及相应的参数信息。
• StatementHandler:封装了JDBi 4 T - E #C Stateme= k & z B *nt操作,负责_ ` q *对JDBC statement的操作H 7,如设置参数、将Statems a 8 F hent结果集转换成List集合等等。
• ParameterHandler:负? R L a K T ` .责对用户传递的参数转换成JDBC Statement 所需要的参数。
• Tyf T z c U = F 8 peHandler:负责Java数据类型和JDBC数据类m O M ; o f ( ]型之间的映射和转换。

我们主要关注获取BoundQ F X ; y a u o @Sql以及参数化语句的流程,这也是3.2.3和3.2.4差异比较大的一个地方。在进入Executor的Query方法后,会首先通过对应b 2 T的MappedStatement来获z + ~ ] . 6 V #取BoundSql,用来帮助我们动态生成SQL语句,里面绑定了对应的SQL以及参数映射关系。在构建框架阶段,我们使用的SqlSource是DynamicSqlSource,通过这个类来生成获取BoundSql,如下图3 e t u Z r6所示:
MyBatis版本升级引发的线上告警回顾及原理分析
图6 获取BoundSql

通过图6的代码,我们可以得知,parameterType在初始化阶段未被使用,而是在SQL执行时获取/ 3 g V 4 C i到的,但获取到的类型是p_ + # $arameterObject对应的类型,这个类是用来记录Mapper方法上对应的参数。如下图7所示,它并非在SQL配置文件中标注的java.lang.String。
MyBatis版本升级引发的线上告警回顾及原理分析
图7 parameterObject类型

然后我们通过SqlSourceBuilder的parse方法对SQL以及获取到的类型进行再次处理,其中的流程代码比较长。在这个过程中,我们主要去构建SQL的参数和Java类型的绑定关系,MyBatis依赖这个绑定关系,使用对应的TypeHandler去进行值的转换。
调用链路是SqlSourceParser.parse -> 内部类 ParameterMappin| , `gTokenHandler.handleToken -> 私有方法 buildParameterMapping,如下图8中的代码所示。/ F X因为当前的parameterType为MapperMethod$ParamMap,经过了多个if判断,判定当前property id的propertyType为Object.class! D z 2 B r j =类型。接下来,构建SQL的参数和Java类型的绑定关系P; n :arameterMapping,再进行返回。
MyBatis版本升级引发的线上告警回顾及原理分析
a b N l 7 } u e8 buildParameterMapping过程

构建完成的ParameterMapping的结构如下图9中的代码所示,参数id对应的javaType类型为java.lang.Object,对应的TypeHander处理器为UnknownTypeHandler,也就是未找到合适的TypeHandler的兜底选项。
MyBatis版本升级引发的线上告警回顾及原理分析
图9 ParameterMapping结构

接下来,流程就会流转到L r B n } gExecutot H q 8r,在org.apache.ibatis.executor.SimpleExecutor#doQuery进行查询时,会根据当前的SQL类型,生成对应的StatementHandler。因为我Q 3 d j }们目前都是用的预编译SQL,因此生成* 7 V ^ O的s8 0 i W = [ =tatementHandler就是PreparedStatementHandler,熟悉JDBC的小伙伴应该马上可以猜到对应的语句是什么类型了。然后,我们对这句SQL语句进行填充,如下图1L L M K ; ( X0中的代码所. P l P ( R E示。我们会通过PreparedStatementHandler的parameterize方法对Statement进行参数化,也就是G r J b c H进行填充。
MyBatis版本升级引发的线上告警回顾及原理分析
图10 PrepareStatement处理过程
在Prepar` ? q l ) M w V DedStatementHandv [ t D kler进行参数化时,会将参数化的职责交给DefaultParame, = D sterHandler处理。如下图11中的代码所示,我们主要关注红线部分,首先会获取ParameterMapping对应的TypeHander,如前文所述,获取到的是UnknownTypeHandler,然R R . o后会通过setParameter方法,将参数id替换成对应的值。
MyBatis版本升级引发的线上告警回顾及原理分析
在Typehandler的流程里,首先会进入BaseTypeHanl l kdler,然后在具体设置时,会进入子类的方法。在UnknownType$ 7 ( vHandler,首先会再次对参数parameter进行解析,判断最正确的TypeHandler类型,如下图12中的代码所示:
MyBatis版本升级引发的线上告警回顾及原理分析
图12 获取可用TypeHandler

在resolveT= Q / s C ` 8 W JypeHandler方法中,因为已知了参数值的类型,通过InB 6 - U wteger这个class在typeHandlerRegis* j 9 atry中寻找对应的Typei ; ! 3Handler,TypeHandleh f & V ;rRegistry是MyBatis启动时C | 2 % M内置好的,代表Java对象类型和TypeHandler的映射关系,有兴趣的同学可以进入这个类详细看下。在这个例子中,我们会直接获取到IntegerHandler,如下图J 2 j E ; 2 L13中的代码所示:
MyBatis版本升级引发的线上告警回顾及原理分析
图13 获取IntegerHandler

在获取到IntegerHandler后,我们就可} * 0 o K }以使用IntegerTypeHandler的setInt方法,对SQL语句中的参数进U F b F l行替换。如图14中的代码所示,SQL语句被成功替换:
MyBatis版本升级引发的线上告警回顾及原理分析
图14 IntegerHander值替换

后续就是执行SQL并处理返回结果,这就不在本文的讨论范围内了。从上文的分析中,我们可以了解到,在3.2.3及以下版本,MyBatis会忽略parameterTyp/ x $ u u Y R .e,在真正进行SQy 8 s xL转换时,重新根据SQL方法入参类型,然后计算合适的TypeHandleL T . ^ F = G q Wr处理器,所以本案例中的代码在T U Z $ R3.2.3版本时,它在运行时是正常的。
以版本3.2.4为例,相比版本3.2.3,MyBatis构建SQL语句过程的变, Z l d化分析
在前一章} ? n _ R J ( X g节中,x T M L n我们得知MyBatis在运行SQL阶段重新计算参数对应的TypeHandler,然后进行SQL参数的替换。那么,在版本3.2.4中,MyBatis做了什么改动,从而导致了原有的使用方式变得不可用呢?从官方的Release Log来看,版本3.2.4做了这样的一个改动。

Thh | x ` 9 U ] zis version builds the binding information during startup and the “paramete# w { Q j hrType” attribute is used

这个意思是说:parameterType会在框架初始化阶段阶段就被使用到。我们将分析的重点放在构建阶段,因为负责处理绑定关系的BoundSql由配置阶段的SqlSo{ c * ( (urce生成,我们主要查看SqlSo} Z W I E nurce的构建,在3.2.4中发生了什么变化。如图15所示,与3.2_ O ~ 1 ! k Y . [.3不同,3.2h l &.4首先判断了是否为动态SQL,在非动态SQL情况下,才会将parameterType java.lang.Strt L % l i & Y sing作为参数,传入Sqlk o @Source的G ! O X W g [ L c构造方法。
MyBatis版本升级引发的线上告警回顾及原理分析
图15 生成SqlSource

而后续流程与3.2.e z f 3 # q 3一致,因为parameter类型为java.lang.String,在构建parameterMapping时,使用的类型就是java.lang.Y G ~ A A &String。_ J x G O
MyBatis版本升级引发的线上告警回顾及原理分析
图16 构建ParameterMapping与3.2.3版本的差异

因为在框架初始化阶段,SqlSource的ParameterMapping中id对应的类型就是java.lang.String,这就导致在进行SQL语句H ~ j e的替换时,获取到的TypeHandler是StringTypeHandler,如下图17所示:
MyBatis版本升级引发的线上告警回顾及原理分析
图17 整数类型的参数获取到了StringTypeHandler

后面的报错原因就比较好理解了,在调用StringTy& b e vpeHandler的setString方法时,报出了java.lang.ClassC$ @ h } y R % %astE$ ) O ) + ?xception: java.lang.Integer cannot b: L u # % ue cast to java.lang.String的错误。
总结
我们总结一下这个案例因:

MyBatis 3.2.o 4 93版本支持parameterType和实际参数类型不匹配,在执行SQL阶段,动态计算值处理器类型。在大版本升级2个版本号后,parameterType实际的类型开始生效,使用对应这个类型的TypeHandler对SQd U T a s ` + r WL进行参数替换,会导致Mapper方法中的参数和XML中的parameterType不匹配O { + j时,进而会出现类型转换报错。

这一段排查的经历,对自己后续编写代码及在系统上线w w a 3时也有一些启发,主要包括以下几个方面:

在i5 # z P - W 7 mnf-bom升级时,需要线下进行全面回归,要避免框架存在不兼容的用法,不然的话,就容易导致线上错误。

开发同学可以检查自己系统内的MyBatis版本,如果是3.2.4以下,需要全面检查下现在3 w / : U m 6的Mapper文件里对于parametT q # ,erType的使用和Mapper方法中实际的参数类型是否一致,避免升级到3.2.4及以上版本时发生转型报错。如果有不匹配的情况存在,需I ! M 4 q ) Q t ^要进行修正或者] f 8 t 9 x @ M不使用parameterType,让MyBatis在运行SQL时自动计算对应的类型。

可以考虑使用MyBatC ] l P P His-Generat^ F ? kor来自动生成XML和Maj M H { R H .pper文件,毕竟是专业团队在维护,稳定性相对来说会更好一些,同时能够避免手动修改XML文件带来的误操作。
可以主动关注强依赖的一些开源框架的Release Log,不要错过了重要9 j o 3的信息。

作者简介

凯伦,2016年校招加入a % 0 ; ) )美团,后端开发工程师。

【编辑推荐】

  1. 服务部署如何做到高可用?这份“三级跳& # J {”秘籍送给你
  2. 让园区网络更聪明!华为AirEngine三新智能升级
  3. 继鸿蒙之后,基于; L ? : U z 2 TopenEuler的商用版本操作系统正式推出,鲲鹏计算生态初具规模
  4. 滴滴万亿级Ela` 7 ?sticSearch平台架构升级解密
  5. EB 级系统空中换引擎:阿里调度执行框架如何全面升级?

【责任编5 { |辑:武晓燕 TEL:(010)68476606】