bug描述
问题起源于同事在项目中新增一个统计用户生日明细的接口,其中一个用户在数据库中的生日日期是“1988-07-29”,然而通过rest接口得到该用户的生日日期却为 “1988-07-28”。
环境说明
开始bug排查之前,先说明下项目环境:
- 系统:centos 7.5
- JDK:1.8.0_171
- 技术栈:spring boot、Jackson、Druid、mybatis、oracle。
bug 排查
从数据层开始查找,先查询数据库时间和时区。
-
SQL> SELECT SYSTIMESTAMP, SESSIONTIMEZONE FROM DUAL;
-
SYSTIMESTAMP SESSIONTIMEZONE
-
-------------------------------------------------------------------------------- ---------------------------------------------------------------------------
-
17-JUL-19 02.20.06.687149 PM +08:00 +08:00
-
SQL>
数据库时间和时区都没有问题。
确认操作系统和java进程时区
- 查看操作系统时区
-
[test@test ~]$ date -R
-
Wed, 17 Jul 2019 16:48:32 +0800
-
[test@test ~]$ cat /etc/timezone
-
Asia/Shanghai
- 查看java进程时区
-
[test@test ~]$ jinfo 7490 |grep user.timezone
-
user.timezone = Asia/Shanghai
可以看出我们操作系统使用的时区和java进程使用的时区一致,都是东八区。
用debug继续往上层查找查看mybatis和JDBC层
查看了问题字段mapper映射字段的jdbcType类型为jdbcType="TIMESTAMP",在mybatis中类型处理注册类TypeHandlerRegistry.java 中对应的处理类为 DateTypeHandler.java。
-
this.register((JdbcType)JdbcType.TIMESTAMP, (TypeHandler)(new DateTypeHandler()));
进一步查看 DateTypeHandler.java 类:
-
//
-
// Source code recreated from a .class file by IntelliJ IDEA
-
// (powered by Fernflower decompiler)
-
//
-
package org.apache.ibatis.type;
-
import java.sql.CallableStatement;
-
import java.sql.PreparedStatement;
-
import java.sql.ResultSet;
-
import java.sql.SQLException;
-
import java.sql.Timestamp;
-
import java.util.Date;
-
public class DateTypeHandler extends BaseTypeHandler<Date> {
-
public DateTypeHandler() {
-
}
-
public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException {
-
ps.setTimestamp(i, new Timestamp(parameter.getTime()));
-
}
-
public Date getNullableResult(ResultSet rs, String columnName) throws SQLException {
-
Timestamp sqlTimestamp = rs.getTimestamp(columnName);
-
return sqlTimestamp != null ? new Date(sqlTimestamp.getTime()) : null;
-
}
-
public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
-
Timestamp sqlTimestamp = rs.getTimestamp(columnIndex);
-
return sqlTimestamp != null ? new Date(sqlTimestamp.getTime()) : null;
-
}
-
public Date getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
-
Timestamp sqlTimestamp = cs.getTimestamp(columnIndex);
-
return sqlTimestamp != null ? new Date(sqlTimestamp.getTime()) : null;
-
}
-
}
因为使用的数据源为Druid,其中 getNullableResult(ResultSet rs, String columnName) 方法参数中 ResultSet使用了DruidPooledResultSet.java 的 getTimestamp(String columnLabel) ,通过列名称获取值然后转换为Date类型的值。