说来惭愧,干了 10 来年程序员,还没有给开源做过任何贡献,以前只知道嘎嘎写,出了问题嘎嘎改,从来没想过提个 PR 去修复他,最近碰到个问题,发现挺简单的,就随手提了个 PR 过去。
问题
问题挺简单的,就是在使用 mybatis 和 ShardingSphere 的时候,有人在 model 类使用了 OffsetDateTime
这个时间类型,发现会报错。
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
这就是一个简单的类型转换的异常,于是跟着源码看了下,先看到BaseTypeHandler#getResult
这个方法,实际上就是根据列名返回查询结果。
根据调用关系,找到了OffsetDateTimeTypeHandler
实现类。
发现最终会调用rs.getObject()
这个方法,那么其实这个方法会最终走到由 ShardingSphere 实现的 getObject
方法中。
看到这里的时候其实已经明白了为啥会报错了,Shardingsphere 只判断了几个LocalDateTime
等类型,对于这个比较特殊的时间类型没有处理,最终会转换成 Timestamp ,然后强转就报错了。
最后调用到ResultSetUtil#convertTimestampValue
方法,可以看到确实是这样哈。
那如果修改源码的话其实很简单了,getObject
判断多加一个,convertTimestampValue
再加一个,就这样很简单啊。
@Override
public T getObject(final int columnIndex, final Class 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;
}
修复
最开始我其实并不想改源码,我在想其他的实现方案,搜索后发现引入一个包就可以解决,也就是 mybatis 的 JSR310 规范。
org.mybatis
mybatis-typehandlers-jsr310
1.0.1
他为什么能解决这个问题?我看了下他的包里面的代码,这不就是加了个 TypeHandler 自己处理了嘛。
再去看了下 OffsetDateTimeTypeHandler
的实现,其实就是自己就解决了,直接给返回OffsetDateTime
,根本不会走到 ShardingSphere 的逻辑里面去,这也就是他能解决这个问题的原因了。
当然,如果不想那么麻烦引入一个包,也可以单独把他拎出来自己去指定一下,这个很简单,就不多说了。
提PR
于是我想,这事情这么简单,我不如提个 PR 给官方吧,这里教下大家怎么提 PR 。
因为不是咱们的项目,是没法 push 代码的,所以进入到项目,然后fork
,fork 好了以后,直接把项目 clone 下来,然后执行命令。
git remote add upstream https://github.com/apache/shardingsphere.git
通过命令我们可以看到成功了,这样就 OK了,然后正常拉分支写代码吧。
写完之后,正常去我们的项目界面提交 PR,然后就可以了。
麻烦
当然,过程并没有这么顺利,虽然说只是很简单的修改。
首先,这个校验就给我提示错误了,第一点叫我不要用 *
号去引用。
这个其实是 IDEA
的锅,如果引用同一个包下类过多的话,会自动帮我们转成星号,这个我们可以在Editor-Code Style-Java
,然后找到 Imports 下的这两个选项,把他们都改成 99 就可以了,防止他自动给我们改成星号。
还有一些其他的比如 if 后面没跟空格之类的,这是我忘记格式化了!
然后大佬回复觉得看不下去,这代码太恶心了,说我们是不是可以用java.time.temporal.TemporalAccessor
来判断,不然这么多时间类型,搞个毛线呢。
然后我就翻译了一段英文,我也不知道大佬看没看懂,我告诉他,这个不好整啊,你看这个接口啊,很多乱七八糟的类实现了他,实际上我觉得我们覆盖常用的一些就行了,其他的特殊时间类型让他们自己用 TypeHandler 处理吧。
大佬说,嗯当然,没办法判断这个接口那我们也没辙了,我说那不可就是嘛。
其实,还有很多时间类型他都会报错的,最好的办法这个都抽象出来和Mybatis单独用实现类,不过那样的话就得大工作了,我太懒了,就这样。
。。。
时间过去了几天,看了下他还没合我代码,是不是嫌弃我?