作为一名有着十年开发经验的老程序员,我最近在项目中遇到了一个关于ShardingSphere和MyBatis整合的奇怪问题。当时我们的系统正在使用ShardingSphere进行分库分表,同时结合MyBatis作为ORM框架。在开发过程中,有同事在实体类中使用了Java 8的OffsetDateTime类型来表示带时区的时间戳,结果运行时却抛出了类型转换异常。
异常堆栈显示,系统试图将java.sql.Timestamp强制转换为java.time.OffsetDateTime时失败了。这个错误看起来很简单,但却暴露了ShardingSphere在处理某些Java 8时间类型时的不足。
提示:如果你也在使用ShardingSphere+MyBatis组合,并且打算使用
OffsetDateTime等Java 8时间API,这篇文章可能会帮你节省不少调试时间。
首先让我们仔细看看这个异常堆栈:
java复制Caused by: java.lang.ClassCastException: class java.sql.Timestamp cannot be cast to class java.time.OffsetDateTime
(java.sql.Timestamp is in module java.sql of loader 'platform'; java.time.OffsetDateTime is in module java.base of loader 'bootstrap')
at org.apache.ibatis.type.OffsetDateTimeTypeHandler.getNullableResult(OffsetDateTimeTypeHandler.java:38)
at org.apache.ibatis.type.OffsetDateTimeTypeHandler.getNullableResult(OffsetDateTimeTypeHandler.java:28)
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:85)
... 99 more
从堆栈可以清晰地看到,问题出在MyBatis的类型处理器(OffsetDateTimeTypeHandler)试图将数据库返回的Timestamp转换为OffsetDateTime时失败了。
我决定深入源码一探究竟。通过调试和阅读源码,我梳理出了以下调用链:
BaseTypeHandler#getResult方法负责根据列名获取查询结果OffsetDateTime类型,会调用OffsetDateTimeTypeHandler的实现getObject方法关键发现是:ShardingSphere的getObject方法实现中,只处理了几种常见的时间类型(LocalDateTime、LocalDate、LocalTime),但没有处理OffsetDateTime。因此,当遇到OffsetDateTime类型时,ShardingSphere会默认返回Timestamp,然后MyBatis尝试强制转换时就失败了。
在考虑修改ShardingSphere源码之前,我先寻找了其他可能的解决方案。经过搜索发现,MyBatis官方提供了一个JSR310规范的扩展包:
xml复制<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.1</version>
</dependency>
这个扩展包为什么能解决问题呢?我查看了它的源码,发现它提供了自己的OffsetDateTimeTypeHandler实现,直接处理了OffsetDateTime类型,完全绕过了ShardingSphere的类型转换逻辑。
虽然引入扩展包可以解决问题,但我认为ShardingSphere本身应该支持更多的Java 8时间类型。于是决定向官方提交PR修复这个问题。
修复思路很简单:
getObject方法中添加对OffsetDateTime的支持ResultSetUtil#convertTimestampValue方法中添加相应的转换逻辑修改后的关键代码如下:
java复制@Override
public <T> T getObject(final int columnIndex, final Class<T> type) throws SQLException {
if (BigInteger.class.equals(type)) {
return (T) BigInteger.valueOf(getLong(columnIndex));
} else if (Blob.class.equals(type)) {
return (T) getBlob(columnIndex);
} else if (Clob.class.equals(type)) {
return (T) getClob(columnIndex);
} else if (LocalDateTime.class.equals(type) || LocalDate.class.equals(type) ||
LocalTime.class.equals(type) || OffsetDateTime.class.equals(type)) {
return (T) ResultSetUtil.convertValue(mergeResultSet.getValue(columnIndex, Timestamp.class), type);
} else {
return (T) ResultSetUtil.convertValue(mergeResultSet.getValue(columnIndex, type), type);
}
}
private static Object convertTimestampValue(final Object value, final Class<?> convertType) {
Timestamp timestamp = (Timestamp) value;
if (LocalDateTime.class.equals(convertType)) {
return timestamp.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
if (LocalDate.class.equals(convertType)) {
return timestamp.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
if (LocalTime.class.equals(convertType)) {
return timestamp.toInstant().atZone(ZoneId.systemDefault()).toLocalTime();
}
if (OffsetDateTime.class.equals(convertType)) {
return timestamp.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime();
}
return value;
}
由于没有直接向ShardingSphere仓库推送代码的权限,我需要先fork项目:
bash复制git clone https://github.com/your-username/shardingsphere.git
bash复制git remote add upstream https://github.com/apache/shardingsphere.git
bash复制git fetch upstream
git checkout -b fix-offsetdatetime upstream/master
bash复制git add .
git commit -m "Add support for OffsetDateTime in ResultSet"
bash复制git push origin fix-offsetdatetime
提交PR后,ShardingSphere的维护者提出了几点改进意见:
代码风格问题:IDEA自动使用了*导入,不符合项目规范
*导入Editor > Code Style > Java > Imports架构建议:维护者建议考虑使用java.time.temporal.TemporalAccessor接口来判断所有时间类型
格式化问题:有些if语句后面缺少空格
Date和java.time API类型转换问题:遇到类型转换异常时,可以按照以下步骤排查:
源码阅读技巧:
这次PR经历让我深刻体会到,即使是看似简单的问题,也可能涉及多个框架的交互。作为开发者,我们不仅要会使用工具,更要理解它们的工作原理。当遇到问题时,勇于深入源码、提出解决方案,不仅能解决自己的问题,还能帮助社区变得更好。
最后,如果你也在使用ShardingSphere并遇到了类似问题,可以考虑以下方案:
记住,开源社区的力量在于共享和协作。你的一个小小贡献,可能会帮助到无数开发者。不要因为觉得自己资历尚浅就不敢参与,每个专家都是从第一个PR开始的。