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

设置服务器时区:

jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai

在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<Time> vf = new SqlTimeValueFactory(this.session.getPropertySet(), cal,
                cal != null ? cal.getTimeZone() : this.session.getServerSession().getDefaultTimeZone());
        return this.thisRow.getValue(columnIndex - 1, vf);
    }    

这个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));
    }

 

"D:\Program Files\Java\jdk-21\bin\java.exe" "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2025.1.2\lib\idea_rt.jar=50688" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath D:\2025小学期java项目\JDBC\target\classes;D:\2025小学期java项目\JDBC\src\main\lib\mysql-connector-java-8.0.18.jar com.itheimajdbc.JDBCDemo Exception in thread "main" java.sql.SQLException: The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support. at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:73) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:76) at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:836) at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:456) at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:246) at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:199) at java.sql/java.sql.DriverManager.getConnection(DriverManager.java:683) at java.sql/java.sql.DriverManager.getConnection(DriverManager.java:230) at com.itheimajdbc.JDBCDemo.main(JDBCDemo.java:19) Caused by: com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support. at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62) at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486) at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61) at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:85) at com.mysql.cj.util.TimeUtil.getCanonicalTimezone(TimeUtil.java:132) at com.mysql.cj.protocol.a.NativeProtocol.configureTimezone(NativeProtocol.java:2121) at com.mysql.cj.protocol.a.NativeProtocol.initServerSession(NativeProtocol.java:2145) at com.mysql.cj.jdbc.ConnectionImpl.initializePropsFromServer(ConnectionImpl.java:1310) at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:967) at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:826) ... 6 more 进程已结束,退出代码为 1 出现什么错误了
最新发布
07-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值