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 的源码并不复杂,如果省去分析它的配置和映射文件解析模块,一个查询 SQLSelectList )的执行流程大致如下:

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

如果用到了内建的 TypeHandler 或者自定义的 TypeHandler 实现,同时出现了参数解析异常,那么很大几率异常就是从 DefaultParameterHandler#setParameters() 方法中出现,这样就能顺藤摸瓜找到出现异常的 TypeHandler

参数解析异常的根本原因

本文前面提到的解析 OffsetDateTime 类型异常,实际上执行查询的时候代码会步入 OffsetDateTimeTypeHandler ,这里对比一下 3.4.53.5.5 版本中 MyBatis 对应的 OffsetDateTimeTypeHandler 实现:

发现了主要区别如下:

3.4.5 版本中,会把 OffsetDateTime 参数类型转换为 Timestamp 类型,再委托到 PreparedStatement#setTimestamp() 进行参数设置。

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

PreparedStatement#setTimestamp() 是很早期的产物,这个方法是没有任何问题的, 3.4.5 版本 MyBatisOffsetDateTime 类型兼容为 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 的开发者应该不可能在这种关键而不复杂的问题上出现纰漏,于是花时间去看看这里的代码提交记录:

这是 Raupach2017-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.JdbcPreparedStatementDataType#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 版本中的 MyBatisOffsetDateTimeTypeHandler ,然后通过配置直接覆盖内置实现即可。

// 假设全类名为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 项目仓库:

Githubhttps://github.com/zjcscut/spring-boot-guide/tree/master/ch9-mybatis-mysql

到此这篇关于MyBatis版本升级导致OffsetDateTime入参解析异常问题复盘的文章就介绍到这了,更多相关MyBatis OffsetDateTime入参异常内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Mybatis foreach标签使用不当导致异常的原因浅析

    异常产生场景及异常信息 上周,由于在Mybatis的Mapper接口方法中使用实现了Map.Entry接口的泛型类,同时此方法对应的sql语句也使用了foreach标签,导致出现了异常.如下为异常信息: org.apache.ibatis.exceptions.PersistenceException: ### Error updating database. Cause: org.apache.ibatis.reflection.ReflectionException: There is no

  • mybatis报错元素内容必须由格式正确的字符数据或标记组成异常的解决办法

    今天同事写一个查询接口的时候,出错:元素内容必须由格式正确的字符数据或标记组成. 错误原因:mybatis查询的时候,需要用到运算符 小于号:< 和大于号: >,在mybatis配置文件里面,这种会被认为是标签,所以解析错误 错误事例: select <include refid="Base_Column_List" /> from t_time_interval where status <> 99 and time_intvl_id >=2

  • 使用Mybatis遇到的there is no getter异常

    在使用mybatis的时候有时候会遇到一个问题就是明明参数是正确的,但是还是会提示There is no getter XXX这个异常,但是一般的解决办法是在mapper里面添加@Param注解来完成是别的,那么为什么会遇到这个问题呢? 以下为举例代码: Mapper层代码 public interface Pro1_Mapper { Pro1_Studnet insertStu(Pro1_Studnet pro1_studnet); } 实体类代码 public class Pro1_Stud

  • Mybatis单个参数的if判断报异常There is no getter for property named 'xxx' in 'class java.lang.Integer'的解决方案

    我们都知道mybatis在进行参数判断的时候,直接可以用<if test=""></if> 就可以了,如下: 1.常规代码 <update id="update" parameterType="com.cq2022.zago.order.entity.Test" > update t_test_l <set > <if test="trnsctWayId != null"

  • MyBatis异常-Property 'configLocation' not specified, using default MyBatis Configuration

    配置文件如下: base-context.xml文件如下: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http

  • MyBatis版本升级导致OffsetDateTime入参解析异常问题复盘

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

  • Mybatis实体类对象入参查询的笔记

    目录 Mybatis实体类对象入参查询 测试实体类对象结构如下 测试文件内容 Mybatis中的参数深入 一.mybatis的参数 parameterType参数 二.mybatis的输出结果的封装 resultType(输出类型) Mybatis实体类对象入参查询 测试实体类对象结构如下 /** 使用lobmok插件 */ @Getter @Setter @NoArgsConstructor @ToString @EqualsAndHashCode public class Vendor {

  • Python基于argparse与ConfigParser库进行入参解析与ini parser

    一.入参解析库 argparse 有时候写Python脚本,需要处理入参[-h][-v][-F]...等情况,如果自己来解析的话,会花费很多时间,而且也容易出问题,好在Python有现成的lib可以使用,就是argparse了,下面我们看看如何使用它. import argparse def get_version(): return "0.0.1" def cmd_handler(): args = argparse.ArgumentParser() args.add_argumen

  • mybatis中的if test判断入参的值问题

    目录 mybatis if test判断入参的值 1.第一种判断方式 2.第二种判断方式 if test动态判断数字时出现的错误 mybatis中if test判断数字 mybatis if test判断入参的值 1.第一种判断方式 <if test=' requisition != null and requisition == "Y" '>    AND 表字段 = #{requisition} </if> 2.第二种判断方式 <if test=&qu

  • mybatis实现获取入参是List和Map的取值

    目录 前言 1.项目结构 2.pom文件配置 3.其他的业务代码 第一种采用#符的取值法 第二种方式采用$符的取值法 4.总结 前言 最近在工作中需要使用到mybatis,需要实现某个功能. 但是发现需要编写一个sql,但是mybatis的映射文件入参是List集合和Map<String,Integer>,需要循环List,然后通过List循环出来的值为Key获取Map中的值作为sql的入参,遇到了一些问题. 但是经过不懈的努力,最后终于解决了这个问题.顺便分享一下自己的经验. 1.项目结构

  • Mybatis调用PostgreSQL存储过程实现数组入参传递

    前言 项目中用到了Mybatis调用PostgreSQL存储过程(自定义函数)相关操作,由于PostgreSQL自带数组类型,所以有一个自定义函数的入参就是一个int数组,形如: 复制代码 代码如下: CREATE OR REPLACE FUNCTION "public"."func_arr_update"(ids _int4)... 如上所示,参数是一个int数组,Mybatis提供了对调用存储过程的支持,那么PostgreSQL独有的数组类型作为存储过程的参数又

  • 详解Mybatis多参数传递入参四种处理方式

    1.利用参数出现的顺序 利用mapper.xml <select id="MutiParameter" resultType="com.jt.mybatis.entity.User"> select * from user where id = #{param1} and username = #{param2} </select> 利用mybatis注解方式(sql语句比较简单时推荐此方式) @Select("select * f

  • 浅谈Mybatis版本升级踩坑及背后原理分析

    1.背景 某一天的晚上,系统服务正在进行常规需求的上线,因为发布时,提示统一的pom版本需要升级,于是从 1.3.9.6 升级至 1.4.2.1. 当服务开始上线后,开始陆续出现了一些更新系统交互日志方面的报警,属于系统辅助流程,报警下图所示, 具体系统数据已脱敏,内容是Mybatis相关的报警,在进行类型转换的时候,产生了强转错误. 更新开票请求返回日志, id:{#######}, response:{{"code":XXX,"data":{"call

  • Spring中使用LocalDateTime、LocalDate等参数作为入参

    0x0 背景 项目中使用LocalDateTime系列作为dto中时间的类型,但是spring收到参数后总报错,为了全局配置时间类型转换,尝试了如下3中方法. 注:本文基于Springboot2.0测试,如果无法生效可能是spring版本较低导致的.PS:如果你的Controller中的LocalDate类型的参数啥注解(RequestParam.PathVariable等)都没加,也是会出错的,因为默认情况下,解析这种参数使用ModelAttributeMethodProcessor进行处理,

随机推荐