• 当时项目中使用的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实现:

    发现了主要区别如下:

    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服务端。然而,「这个问题在2020-7-12最新发布的mysql:mysql-connector-java:8.0.21依然没有解决」

    同样的问题在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的日期时间类型都需要进行兼容,兼容表格如下:

    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项目仓库:

    (本文完 c-2-d e-a-20200802 前段时间搬家带宽一直出问题,断更了接近一周)

    本文分享自微信公众号 - Throwable文摘(throwable-doge)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    09-03 13:10