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 {……//创建ResultSetFactorythis.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 {@Overridepublic 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 caseif (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_ONLYrs = 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);@Overridepublic 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) {}}}
}