java.sql.Time 字段类型匹配MySql的time表字段类型时差问题

开发环境: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));
    }

 

你可能感兴趣的:(Java)