博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Mybatis 3.4.4 升级到3.4.5+版本导致读写操作的时候使用不同的TypeHandler导致异常的解决方案...
阅读量:5738 次
发布时间:2019-06-18

本文共 9392 字,大约阅读时间需要 31 分钟。

项目背景

项目中因需要保留时区信息, 前后台交互采用时间格式为标准ISO8601格式时间, 例如: 2018-11-11T11:48:23.168+08:00,

数据库使用VARCHAR存储. 某日, 系统写入数据依然正常, 但是系统查询突然全部抛异常:

Caused by: java.time.format.DateTimeParseException: Text '2018-11-10 03:11:11.0' could not be parsed at index 10    at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)    at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)    at java.time.OffsetDateTime.parse(OffsetDateTime.java:402)    at java.time.OffsetDateTime.parse(OffsetDateTime.java:387)    at com.example.demo.mapper.typehandler.OffsetDateTimeTypeHandler.getNullableResult(OffsetDateTimeTypeHandler.java:34)    at com.example.demo.mapper.typehandler.OffsetDateTimeTypeHandler.getNullableResult(OffsetDateTimeTypeHandler.java:13)    at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:66)    ... 57 more

建表sql

create table task(  task_id varchar(32) primary key ,  create_at varchar(32) not null);

entity

@Data@EqualsAndHashCode(of = "taskId")public class Task {    private String taskId;    private OffsetDateTime createAt;}

插入sql xml片段:

insert into task(task_id, create_at) values (#{taskId}, #{createAt})

查询sql xml片段

自定义TypeHandler

@MappedTypes(OffsetDateTime.class)@MappedJdbcTypes(value = JdbcType.VARCHAR)public class OffsetDateTimeTypeHandler extends BaseTypeHandler
{ private final DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; @Override public void setNonNullParameter(PreparedStatement ps, int i, OffsetDateTime parameter, JdbcType jdbcType) throws SQLException { if (parameter == null){ ps.setNull(i, Types.VARCHAR); }else { ps.setString(i, formatter.format(parameter)); } } @Override public OffsetDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException { String text = rs.getString(columnName); if (text == null){ return null; } return OffsetDateTime.parse(text); } @Override public OffsetDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String text = rs.getString(columnIndex); if (text == null){ return null; } return OffsetDateTime.parse(text); } @Override public OffsetDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String text = cs.getString(columnIndex); if (text == null){ return null; } return OffsetDateTime.parse(text); }}

从异常信息可知: 是因为 '2018-11-10 03:11:11.0' 字符串转换为 OffsetDateTime出现了错误.

解决方案

  • 第一种: 降低Mybatis版本至3.4.4;
  • 第二种: 在插入sql中指定jdbcType或者指定typehandler(不推荐,因为这样需要修改的地方太多);
    例如, 修改插入sql片段如下:
insert into task(task_id, create_at) values(#{taskId}, #{createAt, jdbcType = VARCHAR})
或者:
insert into task(task_id, create_at) values(#{taskId}, #{createAt, typeHandler = com.example.demo.mapper.typehandler.OffsetDateTimeTypeHandler})
  • 第三种: 修改自定义TypeHandler的@MappedJdbcTypes(推荐)

    由@MappedJdbcTypes(value = JdbcType.VARCHAR)  ---> @MappedJdbcTypes(value = JdbcType.VARCHAR, includeNullJdbcType = true)

或者将自定义的TypeHandler上的 @MappedJdbcTypes(value = JdbcType.VARCHAR)去掉

排查过程

  • 查看数据库表数据, 发现task表数据如下:

    +-----------+-----------------------+  | task_id   | create_at             |  +-----------+-----------------------+  | 123456789 | 2018-11-10 03:11:11.0 |  +-----------+-----------------------+
  • 毫无疑问, 写入数据库的格式发生了改变, 于是开始怀疑有人改动代码,

    接着查询git提交记录, 发现pom.xml文件的依赖发生了改动, 其他源码均无变动, 改动为:

    org.mybatis.spring.boot
    mybatis-spring-boot-starter
    1.3.0
    升级到:
    org.mybatis.spring.boot
    mybatis-spring-boot-starter
    1.3.1
    对应Mybatis版本由3.4.4升级到3.4.5
  • 于是开始怀疑这个问题是由于Mybatis版本升级带来的, 经查看源码发现,mybatis 3.4.5发布的版本里面, 内置了jsr310时间类型的TypeHandler.

    在org.apache.ibatis.type.TypeHandlerRegistry新增了这样一段代码:

    this.register(Instant.class, InstantTypeHandler.class);  this.register(LocalDateTime.class, LocalDateTimeTypeHandler.class);  this.register(LocalDate.class, LocalDateTypeHandler.class);  this.register(LocalTime.class, LocalTimeTypeHandler.class);  this.register(OffsetDateTime.class, OffsetDateTimeTypeHandler.class);  this.register(OffsetTime.class, OffsetTimeTypeHandler.class);  this.register(ZonedDateTime.class, ZonedDateTimeTypeHandler.class);  this.register(Month.class, MonthTypeHandler.class);  this.register(Year.class, YearTypeHandler.class);  this.register(YearMonth.class, YearMonthTypeHandler.class);  this.register(JapaneseDate.class, JapaneseDateTypeHandler.class);
    这也就对应了我们第一个解决方案, 降低Mybatis版本
  • 继续debug发现, Mybatis中TypeHandler是使用一个双层Map存储的:

    private final Map
    >> TYPE_HANDLER_MAP = new ConcurrentHashMap
    >>();

再查看注册TypeHandler的核心代码如下:

private 
void register(Type javaType, TypeHandler
typeHandler) { MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class); if (mappedJdbcTypes != null) { for (JdbcType handledJdbcType : mappedJdbcTypes.value()) { register(javaType, handledJdbcType, typeHandler); } if (mappedJdbcTypes.includeNullJdbcType()) { register(javaType, null, typeHandler); } } else { register(javaType, null, typeHandler); } }

先注册的是Mybatis提供的org.apache.ibatis.type.OffsetDateTimeHandler, 后注册的是我们自定义的OffsetDateTimeHandler.

两个Type的注册信息中, javaType都一样, 区别在于, Mybatis提供的TypeHandler注册信息中二级key(JdbcType)为null, 而我们自定义的TypeHandler二级key为

JdbcType.VARCHAR, 而我们的插入sql片段中,对于jdbcType未指定,默认值也就是null.

所以在写入的时候, 使用的是Mybatis提供的TypeHandler.

通过注册TypeHandler源码,我们发现, 无论是去掉自定义TypeHandler上的@MappedJdbcTypes还是设置这个注解的includeNullJdbcType = true, 都可

以在注册我们自定义的TypeHandler的时候, 替换掉Mybatis提供的TypeHandler.

  • 继续debug, 我们现在还剩下一个疑问, 那为什么在查询的时使用的又是我们自定义的OffsetDateTimeHandler呢?

    这个我们分两点来看:

    5.1. 如何确定jdbcType?

    5.2. 如何确定javaType?

先看第一个问题: 如何确定jdbcType?

在PreparedStatementHandler查找到如下代码:

public 
List
query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.
handleResultSets(ps); }

但是这个就是mybatis的边界了,再继续深入可以查看ps.execute具体在MySql的驱动中如何实现,

现在只知道结果就是mysql提供的驱动包会将查询结果集包装成一个ResultSet具体实现是(com.mysql.cj.jdbc.result.ResultSet),

然后通过ResultSet#getMetaData这个接口就可以拿到每一列的sqlType(sqlType和JdbcType是一一对应的).

再看第二个问题: 如何确定javaType?

就以当前我们自己没有定义任何resultMap来分析一下, 核心代码在DefaultResultSetHandler, 如下:

private List
createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { final String mapKey = resultMap.getId() + ":" + columnPrefix; List
autoMapping = autoMappingsCache.get(mapKey); if (autoMapping == null) { autoMapping = new ArrayList
(); final List
unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix); for (String columnName : unmappedColumnNames) { String propertyName = columnName; if (columnPrefix != null && !columnPrefix.isEmpty()) { // When columnPrefix is specified, // ignore columns without the prefix. if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) { propertyName = columnName.substring(columnPrefix.length()); } else { continue; } } final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase()); if (property != null && metaObject.hasSetter(property)) { if (resultMap.getMappedProperties().contains(property)) { continue; } final Class
propertyType = metaObject.getSetterType(property); if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) { final TypeHandler
typeHandler = rsw.getTypeHandler(propertyType, columnName); autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive())); } else { configuration.getAutoMappingUnknownColumnBehavior() .doAction(mappedStatement, columnName, property, propertyType); } } else { configuration.getAutoMappingUnknownColumnBehavior() .doAction(mappedStatement, columnName, (property != null) ? property : propertyName, null); } } autoMappingsCache.put(mapKey, autoMapping); } return autoMapping; }

大概我们可以看出基本流程就是:

column_name -----> property ----> setterMethod ----> propertyType ----> TypeHandler

例如: create_at 通过在mybatis-config.xml中定义的

得到property为 'createAt', 然后结合ResultType找到createAt的set方法, 找到set方法的参数类型为OffsetDateTime,

也就是 javaType = OffsetDateTime.class, 再结合之前的jdbcType从TypeHandlerRegistry中查找得到TypeHandler为我们自定义的OffsetDateTimeTypeHandler.

转载于:https://www.cnblogs.com/index-xyz/p/9942628.html

你可能感兴趣的文章
web.xml中<load-on-start>n</load-on-satrt>作用
查看>>
【算法】CRF
查看>>
windows 8 微软拼音输入法
查看>>
Windows UI风格的设计(7)
查看>>
SQL中使用WITH AS提高性能 使用公用表表达式(CTE)简化嵌套SQL
查看>>
oracle 强行杀掉一个用户连接
查看>>
Git提交本地库代码到远程服务器的操作
查看>>
mysql中主外键关系
查看>>
我的友情链接
查看>>
让你快速上手的Glide4.x教程
查看>>
浮动和清除(闭合)浮动
查看>>
微信小程序注册流程
查看>>
LR录制脚本时IE打不开的原因
查看>>
类的基础
查看>>
微博自动化测试
查看>>
Sublime Text 2.0.2,Build 2221注册码
查看>>
js scroll事件
查看>>
最长递增子序列 动态规划
查看>>
使用列表
查看>>
原生CSS设置网站主题色—CSS变量赋值
查看>>