0
点赞
收藏
分享

微信扫一扫

Mybatis版本升级踩坑记及背后原理分析

小龟老师 2021-09-28 阅读 74

1、背景

某一天的晚上,系统服务正在进行常规需求的上线,因为发布时,提示统一的pom版本需要升级,于是从 1.3.9.6 升级至 1.4.2.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

报警的一块代码,属于历史功能,失败并不会影响主流程,但在定位期间,会频繁报警,造成一定的干扰,因此当时首先采取回滚操作,将统一的pom版本回滚至历史版本,报警消失,再进行问题的定位和分析。
以下章节是对报警原因的定位及原因详细分析的介绍。

2、报警原因定位

首先是具体的报警原因:

由于mybatis版本由inf-bom引入而来,在inf-bom升级后,由3.2.3 升级至了 3.4.6版本,而Mybatis自3.2.4开始就不支持目前系统内的SQL Mapper的用法,因此上线后,线上出现频繁报警。接下来是定位的过程。

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

1.查看了报警的Mapper方法,如下代码所示, 这个是接收返回参数,根据主键id,更新具体响应内容和时间的代码,入参有3个,类型分别为long, String 和 LocalDateTime

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

2.查看了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>

3.查看了Mybatis上线前后的版本,因为报警的内容是Mybatis处理sql语句时,发现不能将LocalDateTime转型为String,这一段逻辑在上线前是ok的,上线的业务逻辑对这段历史代码无改动,因此猜测是统一pom的升级,导致Mybatis的版本发生了变化,某些历史功能不支持了。mybatis版本上线前后的变化,1.3.9.6对应的版本是3.2.3,1.4.2.1对应的版本是3.4.6。

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

5.为了具体验证第4步的想法,通过UT的方式,通过将Mybatis的版本不断从3.4.6往下降,直至没有报错位置,最终定位是Mybatis版本为3.2.3时,线上代码是正常可用的,只要升一个版本也就是自3.2.4开始,就开始不兼容目前的用法。(这个当时思路不是很好,应该从小版本逐个往上升,可以去加速定位版本的效率)

最后定位报警原因,由于mybatis版本由统一pom引入而来,在统一pom升级后,由3.2.3 升级至了 3.4.6版本,而Mybatis自3.2.4开始就不支持目前系统内的SQL Mapper的用法,因此上线后,线上出现频繁报警。

报警原因已定位,但为什么版本升级后就不兼容历史的用法,并且具体不兼容的是哪一块内容,背后的原理又是什么,请看接下来章节的详细分析。

3、详细分析

3.1 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版本 发生了什么变化。

从官网的Release Log可以看出,Mybatis在3.2.4以前的版本,是忽略XML中的parameterType这个属性,并且使用真实的变量类型进行值的处理,在3.2.4及以后的版本中,这个属性会被启用,因此如果出现类型不匹配的话,就会出现转型失败的报错,也提示我们开发者在升级到这个版本及以上时,需要检查系统内的XML配置,使类型相匹配,或者不设置该属性,让Mybatis自行进行计算。

从以上内容,可以了解到,在版本升级后,mybatis在构建sql语句,获取字段值的时候逻辑发生了变化,那么接下来通过一个普通的示例,了解mybatis在获取字段值这一块的具体代码流程是怎样的,以3.2.3版本为例。

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

在框架初始化阶段,主要有以下流程,如下图所示

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

接下来主要关注SqlSource,这个类会负责在负责生成SQL语句,也是本次案例中,3.2.3和3.2.4差异比较大的地方。接下来会一些源码部分的介绍。

在构建Configuration的过程中,会涉及到构建对应每一条sql语句对应的MappedStatemnt,在parmeterTypeClass就是根据我们在xml配置中写的parmeterType转换而来,值为java.lang.String,在接下来构建SqlSource中,传入了这个参数,如下图所示:

在SqlSource的构建阶段中,parameterType参数其实是被忽略不使用的,这也和官方的描述是一致的,3.2.4之前这个parameterType属性是被忽略的,然后创建了DynamicSqlSource,这个类主要是用于处理Mybatis动态Sql的类。

在框架初始化阶段,需要介绍的内容,在3.2.3版本已经介绍完毕,接下来是当执行getStudentById方法时,Mybatis的流程,如下图所示,受限于图片长度,进行了布局的调整:

在具体执行阶段,也有一些组件,我们需要做了解

接下来主要关注在获取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 sqlSourceParser 对sql以及计算得到的类型进行再次处理,当中流程代码比较长,主要是在这个过程中去制作 sql方法的入参 和 java类型的绑定关系,mybatis依赖这个绑定关系使用对应的TypeHandler去进行值的转换,调用链路是SqlSourceParser.parse -> 内部类 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping, 如下图代码所示。因为当前的parmeterType为 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类型,生成对应的statmentHandler,因为我们目前都是用的预编译SQL,因此生成的statementHandler就是PrepareStatmentHandler,熟悉JDBC的小伙伴应该马上可以猜到这对应的语句是什么类型了。接下来就会对这句SQL语句进行填充,如下图代码所示,会通过PrepareStatmentHandler的parameterize方法对Statment进行参数化,也就是进行填充过程。

在PreparseStatmentHandler进行参数化时,会将参数化的职责交给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会忽略parmeterType,在真正进行sql转换时,重新根据sql方法入参类型计算合适的TypeHandler处理器,所以本案例中的代码在3.2.3时运行时正常的。

3.3 以版本3.2.4为例,相比版本3.2.3,mybatis构建SQL语句过程的变化分析

在3.2章节中,得知mybatis是在运行sql阶段重新计算参数对应的TypeHandler进行sql参数替换,那么在版本3.2.4中,mybatis做了什么改动,导致了原有的使用方式不可用了呢。从官方的release log来看,版本3.2.4做了这样一个改动。

意思是说 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。

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

后面的报错原因就比较好理解了,在调用StringTypeHandler的setString方法时,报出了java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String的错误。

4、总结

总结一下这个案例的主要原因是:

mybatis 3.2.3版本 兼容parameterType和实际参数类型不匹配,运行时动态计算值处理器类型,在大版本升级2个版本号后,parameterType开始生效,以parameterType作为参数的实际类型进行TypeHandler的获取计算,导致类型不匹配时,强转报错。

带给我自己的在后续编写编写代码及系统上线方面的启示是:

1.在统一pom升级时,需要线下进行全面回归,避免框架存在不兼容的用法,导致线上错误。

2.开发同学可以检查自己系统内的mybatis版本,如果是3.2.4以下,需要全面检查下现在的mapper文件里 对于parameterType的使用 和实际的参数类型是否一致,避免升级到3.2.4及以上版本时发生兼容报错,如果有不匹配的情况存在,需要进行修正 或者 不使用parameterType,让Mybatis在运行SQL时自动计算对应的类型,

3.可以考虑使用mybatis-generator来自动生成xml和mapper文件,有专业团队维护,相对来说稳定性更好,也避免自己手动修改xml文件容易带来误操作。

4.可以主动关注强依赖的一些开源框架的Release log,有很多重要的信息。

5、作者简介

岑凯伦、90后软件工程师、5年服务端开发经验 微信公众号: KailunTalk,知识星球:IT编程成长。

举报

相关推荐

0 条评论