Java 程式讀取Mysql資料庫時間資訊與真實時間相差 13、14 小時、SQLException: HOUR_OF_DAY: 2 -> 3

周星猩發表於2020-10-12

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) {
            }
        }
    }
}

 

 

 

 

相關文章