开发环境:MySQL 5.5 / mysql-connector-java 8.0.16 / Spring-boot 2.1.4 / mybatis-spring-boot-starter 2.0.1 / druid-spring-boot-starter 1.1.16
在Java Bean作为查询返回结果集时,Bean里面有个字段为java.sql.Time类型对应MySQL数据库类型为time,但是取出以后时间字段和数据库时间字段相差10小时(最简单的方式可以将bean的时间字段改为String,但是为了查出原因有了下面这一出。另外对于java8, DruidDataSource,bean不能使用LocateTime类型,durid的ResultSet不支持getObject,mybatis对于LocaleTime类型的处理器最终是调用的ResultSet的getObject方法。)。
public class BeanA {
private java.sql.Time aTime;
}
数据库字段类型:
CREATE TABLE `A` (
`id` int(20) UNSIGNED NOT NULL AUTO_INCREMENT ,
`a_time` time NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
Mybatis对于结果集的处理流程:DefaultResultSetHandler -> handleResultSets() -> handleResultSet() -> handleRowValues() -> handleRowValuesForSimpleResultMap() -> getRowValue() -> applyAutomaticMapping() -> TypeHandler -> getResult() -> SqlTimeTypeHandler -> getNullableResult() -> ResultSet -> getTime().
SqlTimeTypeHandler对应处理java.sql.Time, 通过TypeHandlerRegistry注册的默认类型处理器。
因为本例中使用的DruidDataSource,所以ResultSet的包装类为DruidPooledResultSet,在处理getTime时,Mybatis的SqlTimeTypeHandler直接调用的getTime(columeName)签名方法,该签名方法实际实现类是ResultSetImpl(mysql-connector-java), 该实现类中getTime方法重载有多个,但是最终都需要用到一个Calendar对象做时间转换,将mysql的时间类型转换为java.sql.Time。
ResultSetImpl部分源码:
@Override
public Time getTime(String columnName) throws SQLException {
return getTime(findColumn(columnName));
}
@Override
public Time getTime(String columnName, Calendar cal) throws SQLException {
return getTime(findColumn(columnName), cal);
}
@Override
public Time getTime(int columnIndex) throws SQLException {
checkRowPos();
checkColumnBounds(columnIndex);
//这个defaultTimeValueFactory初始化也是SqlTimeValueFactory
return this.thisRow.getValue(columnIndex - 1, this.defaultTimeValueFactory);
}
@Override
public Time getTime(int columnIndex, Calendar cal) throws SQLException {
checkRowPos();
checkColumnBounds(columnIndex);
//最终调用, 注意这个valueFactory,最终是这个处理时间的
ValueFactory
这个SqlTimeValueFactory的实现有个Calendar参数,该Calendar参数里有时区信息,但是调用方mybatis没有用到该方法,所以Calendar是默认的。
public SqlTimeValueFactory(PropertySet pset, Calendar calendar, TimeZone tz) {
super(pset);
if (calendar != null) {
this.cal = (Calendar) calendar.clone();
} else {
// c.f. Bug#11540 for details on locale
//此处的tz是CST,就是TimeZone.getTimeZone("CST")
this.cal = Calendar.getInstance(tz, Locale.US);
this.cal.setLenient(false);
}
}
//该方法是最终从ResultSet取出时间值之后赋给InternalTime转换为java.sql.Time对象,
//debug代码可以看到这个InternalTime在数据库取出时值是正确的,但是转换为Time之后就不对了
@Override
public Time localCreateFromTime(InternalTime it) {
if (it.getHours() < 0 || it.getHours() >= 24) {
throw new DataReadException(
Messages.getString("ResultSet.InvalidTimeValue", new Object[] { "" + it.getHours() + ":" + it.getMinutes() + ":" + it.getSeconds() }));
}
synchronized (this.cal) {
try {
// c.f. java.sql.Time "The date components should be set to the "zero epoch" value of January 1, 1970 and should not be accessed."
this.cal.set(1970, 0, 1, it.getHours(), it.getMinutes(), it.getSeconds());
this.cal.set(Calendar.MILLISECOND, 0);
long ms = (it.getNanos() / 1000000) + this.cal.getTimeInMillis();
return new Time(ms);
} catch (IllegalArgumentException e) {
throw ExceptionFactory.createException(WrongArgumentException.class, e.getMessage(), e);
}
}
}
最终原因就是上面所说的Calendar时区设置问题,默认的时区Calendar为CST, Locate.US,所以取出之后与本地时间不同,也与数据库时间不同。
验证代码:
public static void main(String[] args) throws NoSuchMethodException {
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("CST"), Locale.US);
//17:01:01
InternalTime it = new InternalTime(17,1,1,1);
c.set(1970, 0, 1, it.getHours(), it.getMinutes(), it.getSeconds());
c.set(Calendar.MILLISECOND, 0);
long ms = (it.getNanos() / 1000000) + c.getTimeInMillis();
//输出07:01
System.out.println(new Time(ms));
}