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;@Overridepublic Time getTime(int columnIndex) throws SQLException {checkRowPos();checkColumnBounds(columnIndex);//这个defaultTimeValueFactory初始化也是SqlTimeValueFactoryreturn this.thisRow.getValue(columnIndex - 1, this.defaultTimeValueFactory);}@Overridepublic 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
进行处理
@Overridepublic 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*() callprivate 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 就是 ResultSetImplthis.warningListener = warningListener;}@Overridepublic 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
的序列化并未考虑时区,即使指定时区也不起效。