MyBatis版本升级导致OffsetDateTime入参解析异常问题复盘
背景
最近有一个数据统计服务需要升级SpringBoot的版本,由1.5.x.RELEASE直接升级到2.3.0.RELEASE,考虑到没有用到SpringBoot的内建SPI,升级过程算是顺利。但是出于代码洁癖和版本洁癖,看到项目中依赖的MyBatis的版本是3.4.5,相比当时的最新版本3.5.5大有落后,于是顺便把它升级到3.5.5。升级完毕之后,执行所有现存的集成测试,发现有部分OffsetDateTime类型入参的查询方法出现异常,于是进行源码层面的DEBUG找到最终的问题并且解决。

问题复现
项目中有一个查询方法类似下面的演示例子:
public interface OrderMapper {
List<Order> selectByCreateTime(@Param("startCreateTime") OffsetDateTime startCreateTime,
@Param("endCreateTime") OffsetDateTime endCreateTime);
}
对应的XML文件中的SQL代码段如下:
<select id="selectByCreateTime" resultMap="BaseResultMap">
SELECT *
FROM t_order
WHERE deleted = 0
AND create_time <![CDATA[>=]]> #{startCreateTime}
AND create_time <![CDATA[<=]]> #{e ndCreateTime}
</select>
上面的OrderMapper#selectByCreateTime()方法在MyBatis版本为3.4.5的前提下执行没有任何异常,当MyBatis版本升级为3.5.5后再次执行,在SQL执行日志输出正确的前提下返回了一个空集合,具体的内容如下:
查询订单列表:[]
虽然上帝视角是确认了入参解析有问题,但是基于第一次发生异常的日志,其实定位不到具体发生问题的位置,当时条件反射认为有几处地方会出现这类异常(SQL比较简单,可以排除人为写错SQL占位符的情况):
MyBatis解析OffsetDateTime类型方法参数的方法有版本兼容问题。MySQL驱动包解析OffsetDateTime类型的参数有版本兼容问题。- 前面两种情况混合相互影响导致的,其实这里也可以理解为同一种情况,因为
MyBatis归根到底是对MySQL驱动包进行了封装。
当时项目中使用的mysql-connector-java版本为8.0.18,并未升级为当前的最新版本8.0.21,所以当时也有怀疑是低版本MySQL驱动包没有兼容解析OffsetDateTime类型的参数。
简析MyBatis的执行流程
MyBatis的源码并不复杂,如果省去分析它的配置和映射文件解析模块,一个查询SQL(SelectList)的执行流程大致如下:

当然,因为问题出现在参数解析部分,只需要关注StatementHandler的处理逻辑即可。StatementHandler的父类BaseStatementHandler构造函数中,初始化了ParameterHandler和ResultSetHandler实例,提交到SimpleExecutor中的doQuery()方法中执行,使用了占位符参数的查询会经由doQuery()方法中的prepareStatement()方法然后调用PreparedStatementHandler#parameterize(),最终委托到DefaultParameterHandler#setParameters()方法进行参数设置,这个setParameters()方法会用到ParameterMapping和TypeHandler。

如果用到了内建的TypeHandler或者自定义的TypeHandler实现,同时出现了参数解析异常,那么很大几率异常就是从DefaultParameterHandler#setParameters()方法中出现,这样就能顺藤摸瓜找到出现异常的TypeHandler。
参数解析异常的根本原因
本文前面提到的解析OffsetDateTime类型异常,实际上执行查询的时候代码会步入OffsetDateTimeTypeHandler,这里对比一下3.4.5和3.5.5版本中MyBatis对应的OffsetDateTimeTypeHandler实现:
发现了主要区别如下:
3.4.5版本中,会把OffsetDateTime参数类型转换为Timestamp类型,再委托到PreparedStatement#setTimestamp()进行参数设置。

3.5.5版本中,直接调用PreparedStatement#setObject()进行参数设置。

PreparedStatement#setTimestamp()是很早期的产物,这个方法是没有任何问题的,3.4.5版本MyBatis把OffsetDateTime类型兼容为Timestamp类型处理。那么基本可以确定问题出现在PreparedStatement#setObject()方法上,对于MySQL8.x的驱动,PreparedStatement选用的实现类是com.mysql.cj.jdbc.ClientPreparedStatement,通过层层DEBUG最终到达AbstractQueryBindings#setObject()方法:

由于驱动中没有任何解析OffsetDateTime类型的片段,所以最终会使用AbstractQueryBindings#setSerializableObject()方法(也就是else分支的代码)兜底,直接转化为一个byte[]传输到MySQL服务端,问题就出在这里,直接把OffsetDateTime类型序列化疑似在MySQL服务端拿到的不是预期的参数,导致查询条件出现失效(这里笔者没有花时间去阅读MySQL的协议,也没有花大量时间去抓包,所以这里还只是猜测)。然而,这个问题在2020-7-12最新发布的mysql:mysql-connector-java:8.0.21依然没有解决。但是看到这里又出现一个疑惑,MyBatis的开发者应该不可能在这种关键而不复杂的问题上出现纰漏,于是花时间去看看这里的代码提交记录:

这是Raupach在2017-08-22的一个提交,提交的message是:测试OffsetDateTimeHandler保留了UTC的偏移量。单元测试类OffsetDateTimeTypeHandlerTest也只是验证了TypeHandler#setParameter()和PreparedStatement#setObject()参数传递的正确性,并没有做集成测试去跟踪所有类型数据库的传参问题,估计就是这一步疏忽了,但是这个应该不属于MyBatis的问题,毕竟它只是对数据库驱动包的封装。其中集成测试TimestampWithTimezoneTypeHandlerTest使用了内存数据库,这里可以猜测是HSQLDB驱动完善了日期时间的参数解析。

同样的问题在h2数据库中不会出现,于是稍微DEBUG了一下h2数据库驱动进行参数设置的源码,最终定位到org.h2.value.DataType(驱动包的版本为com.h2database:h2:1.4.200)的第1333行有对应JSR310.OFFSET_DATE_TIME的解析逻辑,所以h2数据库驱动可以支持所有JSR310引入的参数类型的参数值设置。下面的截图是h2数据库驱动中PreparedStatement#setObject()的解析实现(见org.h2.jdbc.JdbcPreparedStatement和DataType#convertToValue()的源码):

这里可见,h2的驱动真的对JDK8+新增的所有日期时间类型都做了解析:

针对问题的解决方案
如果选用了MySQL,这个参数解析异常的问题截至mysql:mysql-connector-java:8.0.21只有一种解决方案:要把OffsetDateTime类型兼容为Timestamp类型进行参数设置。其实对于所有非LocalXX的日期时间类型都需要进行兼容,兼容表格如下:
| 序号 | 类型 | 兼容类型 | 调用方法 |
|---|---|---|---|
| 1 | OffsetDateTime |
Timestamp |
PreparedStatement#setTimestamp() |
| 2 | ZonedDateTime |
Timestamp |
PreparedStatement#setTimestamp() |
| 3 | OffsetDate |
java.sql.Date |
PreparedStatement#setDate() |
| 4 | OffsetTime |
java.sql.Time |
PreparedStatement#setTime() |
以OffsetDateTime为例,只需要参考或者直接使用3.4.5版本中的MyBatis的OffsetDateTimeTypeHandler,然后通过配置直接覆盖内置实现即可。
// 假设全类名为club.throwable.OffsetDateTimeTypeHandler
public class OffsetDateTimeTypeHandler extends BaseTypeHandler<OffsetDateTime> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, OffsetDateTime parameter, JdbcType jdbcType)
throws SQLException {
ps.setTimestamp(i, Timestamp.from(parameter.toInstant()));
}
@Override
public OffsetDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnName);
return getOffsetDateTime(timestamp);
}
@Override
public OffsetDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnIndex);
return getOffsetDateTime(timestamp);
}
@Override
public OffsetDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Timestamp timestamp = cs.getTimestamp(columnIndex);
return getOffsetDateTime(timestamp);
}
private static OffsetDateTime getOffsetDateTime(Timestamp timestamp) {
if (timestamp != null) {
// 这里可以考虑自定义系统的时区,例如ZoneId.of("Asia/Shanghai")
return OffsetDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault());
}
return null;
}
}
配置文件中进行TypeHandler配置覆盖,下面是类路径下配置文件mybatis-config.xml的示例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!--下划线转驼峰-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--未知列映射忽略-->
<setting name="autoMappingUnknownColumnBehavior" value="NONE"/>
</settings>
<typeHandlers>
<!--覆盖内置OffsetDateTimeTypeHandler-->
<typeHandler handler="throwable.club.OffsetDateTimeTypeHandler"/>
</typeHandlers>
</configuration>
其他类型解析异常都可以参照此思路进行兼容。
小结
升级基础框架版本需要谨慎。另外,文中提到的解决方案只是笔者目前通过问题分析和定位得到的一种相对合理的解决方案,也可能有更优解。
本文的demo项目仓库:
Github:https://github.com/zjcscut/spring-boot-guide/tree/master/ch9-mybatis-mysql
(本文完 c-2-d e-a-20200802 前段时间搬家带宽一直出问题,断更了接近一周)
MyBatis版本升级导致OffsetDateTime入参解析异常问题复盘的更多相关文章
- MyBatis各种类型的入参使用方式
https://blog.csdn.net/u011983531/article/details/53561219 mybatis中if判断传入字符串或者Long参数不为空 https://blog. ...
- RobotFramework:发现一个大坑,当post接口入参为json时,千万不能用sojson转化后的json串(ride解析会有异常,非sojson工具问题),直接用浏览器粘过来的就行
问题背景: 和以往一样愉快的进行着自动化测试,突然就不停的提示我,“程序异常”,查看log发现data中的json变为了数组?????? 那算了,我不先组装入参数据直接data=json入参吧,wha ...
- 来啊踩fastjson打印入参导致业务跑偏的坑
线上代码对日志的记录,重要性自不必说.但是怎样记录日志也是有讲究的! 日志可以直接在每个方法中进行日志记录,优点是想怎么记就怎么记,缺点是记日志的代码可能会超过你的业务代码,可读性急剧下降,这也是日志 ...
- Mybatis调用PostgreSQL存储过程实现数组入参传递
注:本文来源于 < Mybatis调用PostgreSQL存储过程实现数组入参传递 > 前言 项目中用到了Mybatis调用PostgreSQL存储过程(自定义函数)相关操作,由于Pos ...
- dubbo接口方法重载且入参未显式指定序列化id导致ClassCastException分析
问题描述&模拟 线上登录接口,通过监控查看,有类型转换异常,具体报错如下图 此报错信息是dubbo consumer端显示,且登录大部分是正常,有少量部分会报类型转换异常,同事通过更换方法名+ ...
- 关于用mybatis调用存储过程时的入参和出参的传递方法
一.问题描述 a) 目前调用读的存储过程的接口定义一般是:void ReadDatalogs(Map<String,Object> map);,入参和出参都在这个map里 ...
- mybatis入参方式和缓冲
1.mybatis入参方式 @Param注解参数(注解) 封装成对象入参 public int updatePassword(@Param("id")int id,@Param(& ...
- mybatis框架之多参数入参--传入Map集合
需求:查询出指定性别和用户角色列表下的用户列表信息 实际上:mybatis在入参的时候,都是将参数封装成为map集合进行入参的,不管你是单参数入参,还是多参数入参,都是可以封装成map集合的,这是无可 ...
- springMVC使用map接收入参 + mybatis使用map 传入查询参数
测试例子: controllel层 ,使用map接收请求参数,通过Debug可以看到,请求中的参数的值都是字符串形式,如果将这个接收参数的map直接传入service,mybatis接收参数时会报错, ...
随机推荐
- TB6560电机驱动器参数设置
TB6560电机驱动器参数设置 最近接触了一些步进电机的开发,整理了一些参数设置的经验,希望能帮助到有需要的人儿~ 步进电机主要按一定的给电规律,给对应的绕组响应的电信号,电机将按一定的方向运行,而且 ...
- python面试题六: 剑指offer
面试题3 二维数组中的查找 LeetCode题目:二维数组中,每行从左到右递增,每列从上到下递增,给出一个数,判断它是否在数组中思路:从左下角或者右上角开始比较 def find_integer(ma ...
- 纯 CSS 实现滑动轮播图效果
只使用css实现轮播图简单的操作 <!DOCTYPE html> <html lang="en"> <head> <meta charse ...
- Shader-内轮廓自发光效果
需求 1 基于涅菲尔反射的变形 原理 (近处的反射少,远处反射多) 1)公式(近似):F = Fscale + (1-Fscale)(1-v·n)^5 利用fresnel做边缘发光,代码 fixed ...
- 作为程序员居然没用过这款神器?太out了吧。
背景 工欲善其事,必先利其器.后面我将陆陆续续推荐一些软件利器帮助大家提高效率(主要针对 Mac 电脑). 如果你在使用 Mac 电脑,并且没有如某些人那样安装并使用 Windows 系统,那么你可 ...
- 你真的清楚DateTime in C#吗?
DateTime,就是一个世界的大融合. 日期和时间,在我们开发中非常重要.DateTime在C#中,专门用来表达和处理日期和时间. 本文算是多年使用DateTime的一个总结,包括DateTim ...
- 利用宝塔和rainloop搭建咱的邮箱
需要咱准备的东东:一枚域名.服务器需根据情况开放25.110.143.465.993端口.宝塔邮局管理器.rainloop.LNMP或者LAMP. 搭建步骤: 1.安装宝塔邮局管理器: 2.设置宝塔邮 ...
- STL源码剖析:算法
启 算法,问题之解法也 算法好坏的衡量标准:时间和空间,单位是对数.一次.二次.三次等 算法中处理的数据,输入方式都是左闭又开,类型就迭代器, 如:[first, last) STL中提供了很多算法, ...
- Linux系统查看硬件信息神器,比设备管理器好用100倍!
大家都知道,当我们的 Linux 系统计算机出现问题时,需要对其排除故障,首先需要做的是找出计算机的硬件信息.下面介绍一个简单易用的应用程序--HardInfo,你可以利用它来显示你电脑的每个硬件方面 ...
- 大汇总 | 一文学会八篇经典CNN论文
本文主要是回顾一下一些经典的CNN网络的主要贡献. 论文传送门 [google团队] [2014.09]inception v1: https://arxiv.org/pdf/1409.4842.pd ...