MyBatis 版本升级引发的线上问题
MyBatis上线前后的版本:上线前(3.2.3)上线后(3.4.6)
服务上线后,开始陆续出现了一些更新系统交互日志方面的报警,这属于系统的辅助流程,报警如下代码所示。我们发现都是跟 MyBatis相关的报警,说明在进行类型转换 [ibatis.type.TypeException]的时候,系统产生了强转错误。
更新开票请求返回日志, 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
报警这一块代码,属于历史功能,如果失败并不会影响主流程。但在定位期间,如果频繁报警的话,就会造成一定的干扰。因此,我们马上采取了回滚操作,直至报警消失,然后再进行问题的定位和分析。
报警原因定位
在回滚完毕后,我们开始具体分析报警产生的主要原因,于是进行了以下几步的排查。
第一步,查看了报警的 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 business_log
SET response = #{response}, update_time = #{updateTime}
WHERE id = #{id}
</update>
第三步,报警的内容是:MyBatis在处理 SQL语句时,发现不能将 LocalDateTime转型为String,这一段逻辑在上线前是可以正常运行的,并且上线的业务逻辑对这段历史代码无改动。因此,我们猜测是因为 MyBatis的版本发生了变化导致的,对某些历史功能不再支持了。MyBatis上线前后的版本:上线前(3.2.3)上线后(3.4.6)
第四步,我们通过第三步可以得到,MyBatis的版本直接升了两个大版本,因此我们可以基本将原因猜测为 MyBatis升级跨度较大,导致部分历史功能没有兼容支持,从而引起线上 SQL的更新报错。
第五步,为了具体验证第四步的想法,我们通过 UT的方式,将 MyBatis的版本不断从 3.4.6往下降,直至没有报错的位置。最终的定位是:当 MyBatis版本为3.2.3时,线上代码是正常可用的,但只要升一个版本,也就是自 3.2.4开始,就开始不兼容目前的用法。不过,我们当时的思路并不是很好,应该从小版本逐个往上升或者使用二分法,可以加速定位版本的效率。
最后,我们定位到了产生报警的根本问题。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的代码非常多,接下来我们主要阐释和本次案例相关的内容。
在框架初始化阶段,主要包括以下流程,如下图所示:
在框架初始化阶段,有一些组件会被构建,逐一做个简单的介绍:
【1】SqlSession:作为 MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
【2】数据库增删改查功能:负责根据用户传递的 parameterObject,动态地生成 SQL语句,将信息封装到 BoundSql对象中,并返回。
【3】Configuration:MyBatis所有的配置信息都维持在 Configuration对象之中。
接下来,我们主要关注 SqlSource,这个类会负责生成SQL语句,这也是本次案例中,3.2.3和3.2.4差异比较大的一个地方。下面,我们会介绍一些源码。
在构建 Configuration的过程中,会涉及到构建对应每一条 SQL语句对应的 MappedStatement,parameterTypeClass就是根据我们在 XML配置中写的 parameterType转换而来,值为 java.lang.String,在构建 SqlSource时,传入这个参数。如下图所示:
在 SqlSource的构建中,parameterType参数其实是被忽略不用的,并没有继续往下传递,这跟官方的描述是一致的。因为 3.2.4之前这个 parameterType属性被忽略了,然后就创建了 DynamicSqlSource,这个类主要是用于处理 MyBatis动态 SQL的类。如下图所示:
在框架初始化的阶段,需要介绍的内容,在 3.2.3版本已经介绍完毕。当执行 getStudentById方法时,MyBatis的流程如下图所示。因受限于图片长度,我们对布局进行了一些调整:
在具体执行阶段,也涉及到一些组件,我们需要做简单的了解:
【1】SqlSession:作为 MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。
【2】Executor:MyBatis执行器,这是 MyBatis调度的核心,负责 SQL语句的生成和查询缓存的维护。
【3】BoundSql:表示动态生成的 SQL语句以及相应的参数信息。
【4】StatementHandler:封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参数、将 Statement结果集转换成 List集合等等。
【5】ParameterHandler:负责对用户传递的参数转换成 JDBC Statement 所需要的参数。
【6】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的 parse方法对 SQL以及获取到的类型进行再次处理,其中的流程代码比较长。在这个过程中,我们主要去构建 SQL的参数和 Java类型的绑定关系,MyBatis依赖这个绑定关系,使用对应的 TypeHandler去进行值的转换。
调用链路是SqlSourceParser.parse -> 内部类 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping,如下图中的代码所示。因为当前的 parameterType为 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类型,生成对应的StatementHandler。因为我们目前都是用的预编译SQL,因此生成的statementHandler就是 PreparedStatementHandler,熟悉 JDBC的小伙伴应该马上可以猜到对应的语句是什么类型了。然后,我们对这句 SQL语句进行填充,如下图中的代码所示。我们会通过PreparedStatementHandler的 parameterize方法对 Statement进行参数化,也就是进行填充。
在 PreparedStatementHandler进行参数化时,会将参数化的职责交给 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会忽略 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中发生了什么变化。如下图所示,与3.2.3不同,3.2.4首先判断了是否为动态SQL,在非动态SQL情况下,才会将 parameterType java.lang.String作为参数,传入SqlSource的构造方法。
而后续流程与3.2.3一致,因为parameter类型为 java.lang.String,在构建 parameterMapping时,使用的类型就是 java.lang.String。
构建 ParameterMapping与3.2.3版本的差异
因为在框架初始化阶段,SqlSource的 ParameterMapping中id对应的类型就是 java.lang.String,这就导致在进行 SQL语句的替换时,获取到的 TypeHandler是 StringTypeHandler,如下图所示:
整数类型的参数获取到了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不匹配时,进而会出现类型转换报错。
这一段排查的经历,对自己后续编写代码及在系统上线时也有一些启发,主要包括以下几个方面:
【1】在项目升级时,需要线下进行全面回归,要避免框架存在不兼容的用法,不然的话,就容易导致线上错误。
【2】开发同学可以检查自己系统内的 MyBatis版本,如果是3.2.4以下,需要全面检查下现在的 Mapper文件里对于 parameterType的使用和 Mapper方法中实际的参数类型是否一致,避免升级到3.2.4及以上版本时发生转型报错。如果有不匹配的情况存在,需要进行修正或者不使用 parameterType,让 MyBatis在运行 SQL时自动计算对应的类型。
【3】可以考虑使用 MyBatis-Generator来自动生成 XML和 Mapper文件,毕竟是专业团队在维护,稳定性相对来说会更好一些,同时能够避免手动修改 XML文件带来的误操作。
【4】可以主动关注强依赖的一些开源框架的 Release Log,不要错过了重要的信息。
MyBatis 版本升级引发的线上问题的更多相关文章
- 一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结
一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结 这是一个十分严重的线上问题 自从最近的某年某月某天起,线上服务开始变得不那么稳定(软病).在高峰期,时常有几台机器的 ...
- 一个SQL注释引发的线上问题
最近开始服务拆分,时间将近半个月.测试阶段也非常顺利,没有什么问题. 但上线之后的第二天,产品就风风火火的来找我们了,一看就是线上有什么问题.我们也不敢说,我们也不敢问,线上的后台商品忽然无法上架了, ...
- 【转】一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结
[转]原文链接:https://cloud.tencent.com/developer/article/1497826 这是一个十分严重的线上问题 自从最近的某年某月某天起,线上服务开始变得不那么稳定 ...
- 一个purge参数引发的惨案——从线上hbase数据被删事故说起
在写这篇blog前,我的心情久久不能平静,虽然明白运维工作如履薄冰,但没有料到这么一个细小的疏漏会带来如此严重的灾难.这是一起其他公司误用puppet参数引发的事故,而且这个参数我也曾被“坑过”. ...
- Spring+SpringMVC+MyBatis+easyUI整合进阶篇(七)一次线上Mysql数据库崩溃事故的记录
作者:13 GitHub:https://github.com/ZHENFENG13 版权声明:本文为原创文章,未经允许不得转载. 文章简介 工作这几年,技术栈在不断更新,项目管理心得也增加了不少,写 ...
- Spring+SpringMVC+MyBatis+easyUI整合进阶篇(八)线上Mysql数据库崩溃事故的原因和处理
前文提要 承接前文<一次线上Mysql数据库崩溃事故的记录>,在文章中讲到了一次线上数据库崩溃的事件记录,建议两篇文章结合在一起看,不至于摸不着头脑. 由于时间原因,其中只讲了当时的一些经 ...
- 简述C#中IO的应用 RabbitMQ安装笔记 一次线上问题引发的对于C#中相等判断的思考 ef和mysql使用(一) ASP.NET/MVC/Core的HTTP请求流程
简述C#中IO的应用 在.NET Framework 中. System.IO 命名空间主要包含基于文件(和基于内存)的输入输出(I/O)服务的相关基础类库.和其他命名空间一样. System.I ...
- 由定时脚本错误以及Elasticsearch配置错误引发的Flink线上事故
近期接手离职同事项目,突然遇到线上事故,Flink无法正常聚合数据生成指标. 以下是详细的排查过程: 问题复现 清晨,运维报告Flink数据分析模块无法正常生成指标数据. 赶紧登陆Flink所在机器, ...
- 记一次真实的线上事故:一个update引发的惨案!
目录 前言 项目背景介绍 要命的update 结语 前言 从事互联网开发这几年,参与了许多项目的架构分析,数据库设计,改过的bug不计其数,写过的sql数以万计,从未出现重大纰漏,但常在河边走,哪 ...
- java运维: 一次线上问题排查所引发的思考
本文转载自 crossoverJie 的b博客 https://www.cnblogs.com/crossoverJie/p/9282065.html 前言 之前或多或少分享过一些内存模型.对象创建之 ...
随机推荐
- eureka注册中心增加登录认证
https://www.cnblogs.com/gxloong/p/12364523.html 开启Eureka注册中心认证 1.目的描述 Eureka自带了一个Web的管理页面,方便我们查询注册 ...
- PostgreSQL 存储过程 通过设定条件,返回指定的数据表记录
PL/pgSQL是 PostgreSQL 数据库系统的一个可装载的过程语言. PL/pgSQL的设计目标是创建一种可装载的过程语言,可以可用于创建函数和触发器过程, 在SQL语言中添加控制结构功能, ...
- Ubuntu22.04 KubeSphere 安装K8S集群
Ubuntu22.04 KubeSphere 安装K8S集群_Ri0n的博客-CSDN博客 一.系统环境系统:Ubuntu 22.04集群IP分布hostname 角色 IP地址master mast ...
- 记录一次 网关负载 流量不均匀 cpu使用率不均衡问题
网关负载 流量不均匀 cpu使用率不均衡问题??? 1.压力机访问源 有多少ip 有10个? 还是20个? 就是样本源不多的话,负载上hash的话 就你可能不是真实的访问需求 ,你客户端就那么 ...
- springboot默认的json配置
springboot默认的json配置 1.@JsonIgnore 返回前端时对应字段不进行序列化返回 public class User { @JsonIgnore private String n ...
- Tacotron2论文阅读笔记
Tacotron2 NATURAL TTS SYNTHESIS BY CONDITIONING WAVENET ON MEL SPECTROGRAM PREDICTIONS论文阅读笔记 先推荐一篇比较 ...
- node后台项目所需中间件梳理
0.nodemon 全局工具,监听项目文件变动,并自动重启项目 一.node内置模块 1.fs fs.readFile() 读取指定文件中的内容fs.writeFile() 向指定的文件中写入内容 ...
- poi读取Excel文件,数字变成科学计数法及数字自动带上“.0”的处理办法
解决poi解析excel遇到数值类型科学计数问题 NumberFormat nf = NumberFormat.getInstance();HSSFCell cell= hssfRow.getCell ...
- 浏览器对象模型(BOM)中的History对象模型
- ajax高级(请求服务器脚本,数据库, ajxa xml文件)
请求jsp与请求普通文件不通过的地方,请求jsp可能会传参,比如搜索,用户名,页码这些 html部分:<input type="text" id="txt1&quo ...