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

发表于 4年以前  | 总阅读数:717 次

本文从一次MyBatis版本升级引发的线上告警开始讲起,然后针对告警定位过程、源码原理进行了深入的分析,并加入了不同版本的类比分析,最后结合实际工作做了一些经验总结,希望能对大家的工程实践有一定的帮助。

背景

某天晚上,美团到店事业群某项系统服务正在进行常规需求的上线。因为在发布时,提示inf-bom版本需要升级,于是我们就将inf-bom版本从1.3.9.6升级至1.4.2.1,如下图1所示:

图1 版本升级

不过,当服务上线后,开始陆续出现了一些更新系统交互日志方面的报警,这属于系统的辅助流程,报警如下方代码所示。我们发现都是跟MyBatis相关的报警,说明在进行类型转换的时候,系统产生了强转错误。

更新开票请求返回日志, 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

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

报警原因定位

在回滚完毕后,我们开始具体分析报警产生的主要原因,于是进行了以下几步的排查。

第一步,查看了报警的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 invoice_log
  SET response = #{response}, update_time = #{updateTime}
WHERE id = #{id}
</update>

第三步,我们查看了MyBatis上线前后的版本,报警的内容是:MyBatis在处理SQL语句时,发现不能将LocalDateTime转型为String,这一段逻辑在上线前是可以正常运行的,并且上线的业务逻辑对这段历史代码无改动。因此,我们猜测是因为inf-bom的升级,从而导致MyBatis的版本发生了变化,对某些历史功能不再支持了。MyBatis版本上线前后的变化如下表所示:

表1 MyBatis版本升级前后对比

第四步,我们通过第三步可以得到,在这次inf-bom的版本升级中,MyBatis的版本直接升了两个大版本,因此我们可以基本将原因猜测为MyBatis升级跨度较大,导致部分历史功能没有兼容支持,从而引起线上SQL的更新报错。

第五步,为了具体验证第四步的想法,我们通过UT的方式,将MyBatis的版本不断从3.4.6往下降,直至没有报错的位置。最终的定位是:当MyBatis版本为3.2.3时,线上代码是正常可用的,但只要升一个版本,也就是自3.2.4开始,就开始不兼容目前的用法。不过,我们当时的思路并不是很好,应该从小版本逐个往上升或者使用二分法,可以加速定位版本的效率。

最后,我们定位到了产生报警的根本问题。总的来说,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并不相同。

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的代码非常多,接下来我们主要阐释和本次案例相关的内容。

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

图2 框架初始化流程

在框架初始化阶段,有一些组件会被构建,逐一做个简单的介绍:

  • SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。

  • SqlSource:负责根据用户传递的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依赖参数

在SqlSource的构建中,parameterType参数其实是被忽略不用的,并没有继续往下传递,这跟官方的描述是一致的。因为3.2.4之前这个parameterType属性被忽略了,然后就创建了DynamicSqlSource,这个类主要是用于处理MyBatis动态SQL的类。如下图4所示:

图4 SqlSource构建

在框架初始化的阶段,需要介绍的内容,在3.2.3版本已经介绍完毕。当执行getStudentById方法时,MyBatis的流程如下图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的代码,我们可以得知,parameterType在初始化阶段未被使用,而是在SQL执行时获取到的,但获取到的类型是parameterObject对应的类型,这个类是用来记录Mapper方法上对应的参数。如下图7所示,它并非在SQL配置文件中标注的java.lang.String。

图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过程

构建完成的ParameterMapping的结构如下图9中的代码所示,参数id对应的javaType类型为java.lang.Object,对应的TypeHander处理器为UnknownTypeHandler,也就是未找到合适的TypeHandler的兜底选项。

图9 ParameterMapping结构

接下来,流程就会流转到Executor,在org.apache.ibatis.executor.SimpleExecutor#doQuery进行查询时,会根据当前的SQL类型,生成对应的StatementHandler。因为我们目前都是用的预编译SQL,因此生成的statementHandler就是PreparedStatementHandler,熟悉JDBC的小伙伴应该马上可以猜到对应的语句是什么类型了。然后,我们对这句SQL语句进行填充,如下图10中的代码所示。我们会通过PreparedStatementHandler的parameterize方法对Statement进行参数化,也就是进行填充。

图10 PrepareStatement处理过程

在PreparedStatementHandler进行参数化时,会将参数化的职责交给DefaultParameterHandler处理。如下图11中的代码所示,我们主要关注红线部分,首先会获取ParameterMapping对应的TypeHander,如前文所述,获取到的是UnknownTypeHandler,然后会通过setParameter方法,将参数id替换成对应的值。

图11 设置参数过程

在Typehandler的流程里,首先会进入BaseTypeHandler,然后在具体设置时,会进入子类的方法。在UnknownTypeHandler,首先会再次对参数parameter进行解析,判断最正确的TypeHandler类型,如下图12中的代码所示:

图12 获取可用TypeHandler

在resolveTypeHandler方法中,因为已知了参数值的类型,通过Integer这个class在

typeHandlerRegistry中寻找对应的TypeHandler,TypeHandlerRegistry是MyBatis

启动时内置好的,代表Java对象类型和TypeHandler的映射关系,有兴趣的同学可以进

入这个类详细看下。在这个例子中,我们会直接获取到IntegerHandler,如下图13中的

代码所示:

图13 获取IntegerHandler

在获取到IntegerHandler后,我们就可以使用IntegerTypeHandler的setInt方法,对

SQL语句中的参数进行替换。如图14中的代码所示,SQL语句被成功替换:

图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

而后续流程与3.2.3一致,因为parameter类型为java.lang.String,在构建parameterMapping时,使用的类型就是java.lang.String。

图16 构建ParameterMapping与3.2.3版本的差异

因为在框架初始化阶段,SqlSource的ParameterMapping中id对应的类型就是java.lang.String,这就导致在进行SQL语句的替换时,获取到的TypeHandler是StringTypeHandler,如下图17所示:

图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,不要错过了重要的信息。

作者:凯伦

本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/VR2KHAesxiQJ42g3abq_5A

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录