CST时区引起的异常:

Java 程序读取Mysql数据库时间信息,与真实时间相差 13、14 小时
java.sql.SQLException: HOUR_OF_DAY: 2 -> 3

原因:

Mysql 驱动:mysql-connector-java 升级到8版本后。将数据库时间解析到java时间,需要获取数据库的时区。

java如何数据库时区:
1、数据库连接中指明的时区,就用该时区,优先级最高。datasource.urljdbc:mysql://127.0.0.1:3306/yourDB?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。
2、通过语句查询数据时区 select @@time_zone,如果返回SYSTEM,数据库没有配置时区,使用系统时区 select @@system_time_zone。

mysql> select @@time_zone;
+-------------+
| @@time_zone |
+-------------+
| SYSTEM      |
+-------------+

mysql> select @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| CST                |
+--------------------+

如果java获取数据库的时区是CST,就会出现以上的问题。

CST是什么时区?
CST时间有四种解释:
美国中部时间 Central Standard Time (USA) UTC-06:00/ UTC-05:00 
澳大利亚中部时间 Central Standard Time (Australia) UTC+09:30
中国标准时 China Standard Time UTC+08:00
古巴标准时 Cuba Standard Time UTC-04:00

java是美国人的,当然认为CST是美国中部时间。

夏令时:
由于美国有夏令时,CST非夏令时对应 UTC-06:00,夏令时对应 UTC-05:00 。
美国的夏令时,从每年3月第2个星期天凌晨开始,到每年11月第1个星期天凌晨结束。
以2020年为例:
夏令时开始时间调整前:2020年03月08日星期日 02:00:00,时间向前拨一小时.
调整后:2020年03月08日星期日 03:00:00

夏令时结束时间调整前:2020年11月01日星期日 02:00:00,时间往回拨一小时.
调整后:2020年11月01日星期日 01:00:00

这意味这:CST没有2020-03-08 02:00:00~2020-03-08 03:00:00 这个区间的时间。会有两个 2020-11-01 01:00:00~2020-11-01 02:00:00区间的时间。

例:
//相差14小时
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("CST"), Locale.US);
calendar.setLenient(false);
//2020-03-08 01:02:00
calendar.set(2020, 2, 8, 1, 2, 0);

Date s = new Date(calendar.getTimeInMillis());
SimpleDateFormat f1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(f1.format(s));//2020-03-08 15:02:00
//2020-03-08 01:02:00 非夏令时间,于北京时间相差14小时。


例:
//相差13小时
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("CST"), Locale.US);
calendar.setLenient(false);
//2020-03-08 03:02:00
calendar.set(2020, 2, 8, 3, 2, 0);

Date s = new Date(calendar.getTimeInMillis());
SimpleDateFormat f1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(f1.format(s));//2020-03-08 16:02:00
//2020-03-08 03:02:00 夏令时间,于北京时间相差13小时。

例:
//抛出 Exception: HOUR_OF_DAY: 2 -> 3
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("CST"), Locale.US);
calendar.setLenient(false);//严格数据校验
//2020-03-08 02:02:00
calendar.set(2020, 2, 8, 2, 2, 0);

Date s = new Date(calendar.getTimeInMillis());//抛出异常:java.lang.IllegalArgumentException: HOUR_OF_DAY: 2 -> 3
SimpleDateFormat f1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(f1.format(s));
//2020-03-08 02:02:00 在CST 时区中是一个不存在的时间,因此出现了异常。

解决上面的问题3种方式:
1、修改数据时区。
2、降低mysql 驱动 mysql-connector-java的版本。
3、访问数据库连接上加上数据时区。这种方式最好。

 

mysql-connector-java 源码中是如何操作的

 

public class ConnectionImpl implements JdbcConnection, SessionEventListener, Serializable {

    private NativeSession session = null;

    protected ResultSetFactory nullStatementResultSetFactory;

    public ConnectionImpl(HostInfo hostInfo) throws SQLException {
        ……
        try {
            ……
            //创建ResultSetFactory
            this.nullStatementResultSetFactory = new ResultSetFactory(this, null);
            //创建session 
            this.session = new NativeSession(hostInfo, this.propertySet);
            ……
        } catch (CJException e1) {
            throw SQLExceptionsMapping.translateException(e1, getExceptionInterceptor());
        }
        try {
            // createNewIO方法 -> connectOneTryOnly方法 -> initializePropsFromServer方法
            createNewIO(false);
        } catch (SQLException ex) {
        }
        ……
    }


    private void initializePropsFromServer() throws SQLException {
        ……
        //配置session ,这里就有获取数据库时区的操作
        this.session.getProtocol().initServerSession();
        ……
    }
}

public class NativeProtocol extends AbstractProtocol<NativePacketPayload> implements Protocol<NativePacketPayload>, RuntimePropertyListener {

	@Override
    public void initServerSession() {
		//配置时区               
        configureTimezone();
    }
     /**
     * 配置时区
     */
    public void configureTimezone() {
        //获取数据库时区	
        String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");
		//获取数据库系统时区
        if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
            configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
        }
		//获取访问数据连接中配置的时区
        String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();

        if (configuredTimeZoneOnServer != null) {
            // user can override this with driver properties, so don't detect if that's the case
            if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {
                try {
                    canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
                } catch (IllegalArgumentException iae) {
                    throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor());
                }
            }
        }

        if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
            this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));

            //
            // The Calendar class has the behavior of mapping unknown timezones to 'GMT' instead of throwing an exception, so we must check for this...
            //
            if (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverSession.getServerTimeZone().getID().equals("GMT")) {
                throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }),
                        getExceptionInterceptor());
            }
        }

        this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
    }
}

 

在创建Connect时同样也创建了ResultSetFactory ,该Factory会创建ResultSetImpl ,ResultSetImpl用于解析数据库返回的结果集。

public class ResultSetFactory implements ProtocolEntityFactory<ResultSetImpl, NativePacketPayload> {

    private JdbcConnection conn;
    private StatementImpl stmt;

    public ResultSetFactory(JdbcConnection connection, StatementImpl creatorStmt) throws SQLException {
        this.conn = connection;
        this.stmt = creatorStmt;
    }

    /**
     * 创建ResultSetImpl
     */
    public ResultSetImpl createFromResultsetRows(int resultSetConcurrency, int resultSetType, ResultsetRows rows) throws SQLException {

        ResultSetImpl rs;

        StatementImpl st = this.stmt;

        if (rows.getOwner() != null) {
            st = ((ResultSetImpl) rows.getOwner()).getOwningStatement();
        }

        switch (resultSetConcurrency) {
            case java.sql.ResultSet.CONCUR_UPDATABLE:
                rs = new UpdatableResultSet(rows, this.conn, st);
                break;

            default:
                // CONCUR_READ_ONLY
                rs = new ResultSetImpl(rows, this.conn, st);
                break;
        }

        return rs;
    }
}
public class ResultSetImpl extends NativeResultset implements ResultSetInternalMethods, WarningListener {


    protected volatile JdbcConnection connection;

    protected NativeSession session = null;


    public ResultSetImpl(ResultsetRows tuples, JdbcConnection conn, StatementImpl creatorStmt) throws SQLException {
        this.connection = conn;
        this.session = (NativeSession) conn.getSession();

        ……
        this.floatValueFactory = new FloatValueFactory(pset);
        this.doubleValueFactory = new DoubleValueFactory(pset);
        this.bigDecimalValueFactory = new BigDecimalValueFactory(pset);
        this.binaryStreamValueFactory = new BinaryStreamValueFactory(pset);
        this.defaultDateValueFactory = new SqlDateValueFactory(pset, null, this.session.getServerSession().getDefaultTimeZone(), this);
        this.defaultTimeValueFactory = new SqlTimeValueFactory(pset, null, this.session.getServerSession().getDefaultTimeZone(), this);
        //这里会将session中的时区传入                            
        this.defaultTimestampValueFactory = new SqlTimestampValueFactory(pset, null, this.session.getServerSession().getDefaultTimeZone());
        this.defaultLocalDateValueFactory = new LocalDateValueFactory(pset, this);
        this.defaultLocalTimeValueFactory = new LocalTimeValueFactory(pset, this);
        ……

    }
}
public class SqlTimestampValueFactory extends AbstractDateTimeValueFactory<Timestamp> {

    private Calendar cal;


    public SqlTimestampValueFactory(PropertySet pset, Calendar calendar, TimeZone tz) {
        super(pset);
        if (calendar != null) {
            this.cal = (Calendar) calendar.clone();
        } else {
            this.cal = Calendar.getInstance(tz, Locale.US);
            this.cal.setLenient(false);


    @Override
    public Timestamp localCreateFromTimestamp(InternalTimestamp its) {

        synchronized (this.cal) {
            try {
              
                this.cal.set(its.getYear(), its.getMonth() - 1, its.getDay(), its.getHours(), its.getMinutes(), its.getSeconds());
                Timestamp ts = new Timestamp(this.cal.getTimeInMillis());
                ts.setNanos(its.getNanos());
                return ts;
            } catch (IllegalArgumentException e) {
            }
        }
    }
}

 

 

 

 

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐