java.sql.Time 字段时区问题 系列文章目录
第一章 初步分析
第二章 Mybatis 源码分析
第三章 Jackson 源码分析 意想不到的Time处理类
文章目录
- java.sql.Time 字段时区问题 系列文章目录
- 前言
- Mybatis源码阅读
- 1. ResultSetImpl部分源码:
- 2. SqlTimeValueFactory部分源码:
- 2.1 SqlTimeValueFactory debug 截图
- 分析
- 总结
前言
初步分析
文中,主要针对项目部署服务器时区、数据库时区、Jvm运行设置时区和java.sql.Time
字段序列化过程时区问题进行展开分析。并给出三个可能问题相对应的解决方案。但是,前段时间又出现时区问题。让我必须重新思考此问题。
以下内容主要对Mybatis源码进行阅读,理解分析java.sql.Time
字段持久化过程,并定位时区问题。
Mybatis源码阅读
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
。
1. ResultSetImpl部分源码:
/*
* This program is also distributed with certain software (including but not
* limited to OpenSSL) that is licensed under separate terms, as designated in a
* particular file or component or in included license documentation. The
* authors of MySQL hereby grant you an additional permission to link the
* program and your derivative works with the separately licensed software that
* they have included with MySQL.
*
* Without limiting anything contained in the foregoing, this file, which is
* part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
* version 1.0, a copy of which can be found at
* http://oss.oracle.com/licenses/universal-foss-exception.
*/
package com.mysql.cj.jdbc.result;
import java.sql.Time;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
import java.util.TimeZone;
public class ResultSetImpl extends NativeResultset implements ResultSetInternalMethods, WarningListener {
/** Time字段内容生成仓库 实际初始化 SqlTimeValueFactory */
private ValueFactory<Time> defaultTimeValueFactory;
@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().getServerTimeZone());
return this.thisRow.getValue(columnIndex - 1, vf);
}
}
从下面代码可以看出来,处理java.sql.Time
字段主要是通过 SqlTimeValueFactory
进行处理
@Override
public Time getTime(int columnIndex, Calendar cal) throws SQLException {
checkRowPos();
checkColumnBounds(columnIndex);
ValueFactory<Time> vf = new SqlTimeValueFactory(this.session.getPropertySet(), cal,
cal != null ? cal.getTimeZone() : this.session.getServerSession().getServerTimeZone());
return this.thisRow.getValue(columnIndex - 1, vf);
}
2. SqlTimeValueFactory部分源码:
/*
*
* This program is also distributed with certain software (including but not
* limited to OpenSSL) that is licensed under separate terms, as designated in a
* particular file or component or in included license documentation. The
* authors of MySQL hereby grant you an additional permission to link the
* program and your derivative works with the separately licensed software that
* they have included with MySQL.
*
* Without limiting anything contained in the foregoing, this file, which is
* part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
* version 1.0, a copy of which can be found at
* http://oss.oracle.com/licenses/universal-foss-exception.
*/
package com.mysql.cj.result;
import java.sql.Time;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import com.mysql.cj.WarningListener;
import com.mysql.cj.conf.PropertySet;
import com.mysql.cj.protocol.InternalTime;
/**
* A value factory to create {@link java.sql.Time} instances. As with other date/time types, a time zone is necessary to interpret the
* time values returned from the server.
*/
public class SqlTimeValueFactory extends AbstractDateTimeValueFactory<Time> {
private WarningListener warningListener;
// cached per instance to avoid re-creation on every create*() call
private Calendar cal;
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
// 此处 TimeZone tz 时区是通过 连接Session获取时区,即连接Mysql数据库的时区设置
this.cal = Calendar.getInstance(tz, Locale.US);
this.cal.setLenient(false);
}
}
public SqlTimeValueFactory(PropertySet pset, Calendar calendar, TimeZone tz, WarningListener warningListener) {
this(pset, calendar, tz);
// warningLinster 就是 ResultSetImpl
this.warningListener = warningListener;
}
@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);
}
}
}
}
InternalTime
源码:本身就是一个内部时间,和时区无关
/*
* This program is also distributed with certain software (including but not
* limited to OpenSSL) that is licensed under separate terms, as designated in a
* particular file or component or in included license documentation. The
* authors of MySQL hereby grant you an additional permission to link the
* program and your derivative works with the separately licensed software that
* they have included with MySQL.
*
* Without limiting anything contained in the foregoing, this file, which is
* part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
* version 1.0, a copy of which can be found at
* http://oss.oracle.com/licenses/universal-foss-exception.
*/
package com.mysql.cj.protocol;
public class InternalTime {
private int hours = 0;
private int minutes = 0;
private int seconds = 0;
private int nanos = 0;
private int scale = 0;
/**
* Constructs a zero time
*/
public InternalTime() {
}
public InternalTime(int hours, int minutes, int seconds, int nanos, int scale) {
this.hours = hours;
this.minutes = minutes;
this.seconds = seconds;
this.nanos = nanos;
this.scale = scale;
}
}
通过对源码查看可以知道,java.sql.Time
字段的持久化过程中,真正调用了localCreateFromTime()
方法,获取持久化生成的InternalTime对象
,设置Calendar
对象的时间参数来生成java.sql.Time
对象,实际生成过程中使用的以1970年1月1日00:00:00 GMT标准基准时间的毫秒数,并没有由于时区问题做时间的计算更改。
实际核心代码如下:
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();
2.1 SqlTimeValueFactory debug 截图
分析
通过对以上源码的分析,知道Mybatis如何把数据库中Time
类型字段持久化成java.sql.Time
对象,并且持久化过程中并未发现有关时区问题。
但是由于java.sql.Time
类是继承java.util.Date
类并屏蔽年月日的类,还是和时区有关,以下测试可以看出来:
同一个Time
对象在不同时区下展示日内时间是随时区变化而变化的。
总结
通过以上分析可以得出,Mybatis有对java.sql.Time
字段专门处理类,过程正确无误,并不存在时区问题。
但是生成Time
对象在不同时区下展示日内时间是随时区变化而变化的。因此,如果项目中出现时区问题,还是可能是以下情况:
- 服务器时区,突然被改变
- jvm时区,突然被改变
- jackson 对
java.sql.Time
的序列化并未考虑时区,即使指定时区也不起效。