项目中使用slic 3.1.0 版本,配合mysql-connector-java-5.1.24,当有需求将jdbc升级至高版本以适配可能的mysql 8.0版本时,发现插入数据库的时间比实际早了14个小时
从代码部分出发,问题出在slick的update函数,我们的代码调用update更新数据库的部分类型为Timestamp的字段,slick包中的update函数源码
// slick_2.11-3.1.0.jar
package slick.driver
/** An Action that updates the data selected by this query. */
def update(value: T): DriverAction[Int, NoStream, Effect.Write] = {
new SimpleJdbcDriverAction[Int]("update", Vector(sres.sql)) {
def run(ctx: Backend#Context, sql: Vector[String]): Int = ctx.session.withPreparedStatement(sql.head) { st =>
st.clearParameters
converter.set(value, st)
sres.setter(st, converter.width+1, param)
st.executeUpdate
}
}
}
传入参数value即为要更新的值,如下图,可见到此处的时间还是正确的。update函数通过UPDATE TABLE_NAME SET status = ?, time1 = ? , time2 = ? where TABLE_NAME.ID = 233;
,结合value参数,转化为实际的update sql语句,然后由executeUpdate执行拼接好的sql语句。
运行至set函数内部,源码如下,会根据value的类型T对其做转换,本例中是将代码中的java.util.Date
类型转化为java.sql.Timestamp
,会调用jdbc包中的setTimestamp函数
def set(value: T, pp: Writer) = {
var i = 0
while(i < len) {
cha(i).asInstanceOf[ResultConverter[M, Any]].set(value.productElement(i), pp)
i += 1
}
}
此处两个版本差别较大,部分源码如下。
targetCalendar
为用户是否设置TimeZone,即请求url中是否有类似serverTimezone=Asia/Shanghai
,本例未加,因此targetCalendar=null
,其this.tsdf.calendar.zone如下图
serverTimezone=Asia/Shanghai
,this.tsdf.calendar.zone
如下图// mysql-connector-java-8.0.13.
package com.mysql.cj;
@Override
public void setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {
... ...
this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,
targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone());
... ...
setTimestampInternal
的第四个参数,其决定了内部参数的时区,即setTimestampInternal
的tz参数,如下图可见,其ZoneInfo的 id = Asia/Shanghai, offset = 28800000
,即为中国标准时区/ mysql-connector-java-5.1.24.jar
package com.mysql.jdbc;
/**
* Set a parameter to a java.sql.Timestamp value. The driver converts this
* to a SQL TIMESTAMP value when it sends it to the database.
*
*/
public void setTimestamp(int parameterIndex, Timestamp x)
throws java.sql.SQLException {
setTimestampInternal(parameterIndex, x, null, Util.getDefaultTimeZone(), false);
}
/**
* Set a parameter to a java.sql.Timestamp value. The driver converts this
* to a SQL TIMESTAMP value when it sends it to the database.
*/
private void setTimestampInternal(int parameterIndex,
Timestamp x, Calendar targetCalendar,
TimeZone tz, boolean rollForward)
在程序一开始,TimeZone初始为空,则调用getDefaultRef
为其设置一个初始值,如下所示,根据调用链显示,最终调用native方法getSystemTimeZoneID
,传入参数为javaHome,设置完后this.defaultTimeZone.getID() = Asia/Shanghai
,两个版本表现一致。
package java.util.TimeZone
/**
* Returns the reference to the default TimeZone object. This
* method doesn't create a clone.
*/
static TimeZone getDefaultRef() {
TimeZone defaultZone = defaultTimeZone;
if (defaultZone == null) {
// Need to initialize the default time zone.
defaultZone = setDefaultZone();
assert defaultZone != null;
}
// Don't clone here.
return defaultZone;
}
private static synchronized TimeZone setDefaultZone() {
TimeZone tz;
// get the time zone ID from the system properties
String zoneID = AccessController.doPrivileged(
new GetPropertyAction("user.timezone"));
// if the time zone ID is not set (yet), perform the
// platform to Java time zone ID mapping.
if (zoneID == null || zoneID.isEmpty()) {
String javaHome = AccessController.doPrivileged(
new GetPropertyAction("java.home"));
try {
zoneID = getSystemTimeZoneID(javaHome);
if (zoneID == null) {
zoneID = GMT_ID;
}
} catch (NullPointerException e) {
zoneID = GMT_ID;
}
}
// Get the time zone for zoneID. But not fall back to
// "GMT" here.
tz = getTimeZone(zoneID, false);
if (tz == null) {
// If the given zone ID is unknown in Java, try to
// get the GMT-offset-based time zone ID,
// a.k.a. custom time zone ID (e.g., "GMT-08:00").
String gmtOffsetID = getSystemGMTOffsetID();
if (gmtOffsetID != null) {
zoneID = gmtOffsetID;
}
tz = getTimeZone(zoneID, true);
}
... ...
return tz;
}
/**
* Gets the platform defined TimeZone ID.
**/
private static native String getSystemTimeZoneID(String javaHome);
前文提过,jdbc每次connect之前,都会通过configureTimezone
设置时区,源码如下,同样分为8.0.13版本和5.1.24版本
// 8.0.13
package com.mysql.cj.protocol.a;
/**
* Configures the client's timezone if required.
*
* @throws CJException
* if the timezone the server is configured to use can't be
* mapped to a Java timezone.
*/
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());
}
}
}
// canonicalTimezone = "CST"
if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));
... ...
this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
}
// 5.1.24
package com.mysql.jdbc;
/**
* Configures the client's timezone if required.
*
* @throws SQLException
* if the timezone the server is configured to use can't be
* mapped to a Java timezone.
*/
private void configureTimezone() throws SQLException {
String configuredTimeZoneOnServer = this.serverVariables.get("timezone");
if (configuredTimeZoneOnServer == null) {
configuredTimeZoneOnServer = this.serverVariables.get("time_zone");
if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
configuredTimeZoneOnServer = this.serverVariables.get("system_time_zone");
}
}
String canoncicalTimezone = getServerTimezone();
if ((getUseTimezone() || !getUseLegacyDatetimeCode()) && configuredTimeZoneOnServer != null){... ...}
// canoncicalTimezone = null
if (canoncicalTimezone != null && canoncicalTimezone.length() > 0) {
this.serverTimezoneTZ = TimeZone.getTimeZone(canoncicalTimezone);
... ...
}
}
canonicalTimezone = "CST"
,因此会进入下面的if分支,调用TimeZone.getTimeZone()
,传入参数”CST“,输出时区信息如下canonicalTimezone = null
,因此不会进入if分支,直接退出,采用默认时区,即前文采用native方法getSystemTimeZoneID
获取的Asia/Shanghai// 病根
// CST -> 美国时区
// Asia/Shanghai -> 中国八区
private static TimeZone getTimeZone(String ID, boolean fallback) {
TimeZone tz = ZoneInfo.getTimeZone(ID);
if (tz == null) {
tz = parseCustomTimeZone(ID);
if (tz == null && fallback) {
tz = new ZoneInfo(GMT_ID, 0);
}
}
return tz;
}
两个版本对时区默认设置的策略不同,如下表
5.1.24 | 8.0.13 | |
---|---|---|
不显式设置时区 | 采用系统默认(native) | 采用mysql server时区(CST) |
显式设置 | 设置优先 | 设置优先 |