成都网站建设设计

将想法与焦点和您一起共享

怎么排查因JDK导致接口输出日期格式的时间与预期时间不一致问题

本篇内容主要讲解“怎么排查因JDK导致接口输出日期格式的时间与预期时间不一致问题”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么排查因JDK导致接口输出日期格式的时间与预期时间不一致问题”吧!

成都创新互联是一家集网站建设,鄂温克企业网站建设,鄂温克品牌网站建设,网站定制,鄂温克网站建设报价,网络营销,网络优化,鄂温克网站推广为一体的创新建站企业,帮助传统企业提升企业形象加强企业竞争力。可充分满足这一群体相比中小企业更为丰富、高端、多元的互联网需求。同时我们时刻保持专业、时尚、前沿,时刻以成就客户成长自我,坚持不断学习、思考、沉淀、净化自己,让我们为更多的企业打造出实用型网站。

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 {
  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类型的值。

怎么排查因JDK导致接口输出日期格式的时间与预期时间不一致问题

由上图debug看到 Timestamp 是JDK中的类,也就是说这里看到的是JDK使用的时间和时区,从图中标注2处可以看出JDK使用的时区也是东八区,但是从1和3处看起来似乎有点不一样,首先1处变化为UTC/GMT+0900,3处有一个daylightSaving的这样一个时间,换算为小时刚好为1个小时。这个值通过google搜索知道叫做夏令时。

常用时间概念 UTC,GMT,CST,DST
  • UTC协调世界时(英语:Coordinated Universal Time,法语:Temps Universel Coordonné,简称UTC)是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林尼治标准时间。中华民国采用CNS 7648的《资料元及交换格式–资讯交换–日期及时间的表示法》(与ISO 8601类似)称之为世界协调时间。中华人民共和国采用ISO 8601:2000的国家标准GB/T 7408-2005《数据元和交换格式 信息交换 日期和时间表示法》中亦称之为协调世界时。(摘自:https://zh.wikipedia.org/wiki/%E5%8D%8F%E8%B0%83%E4%B8%96%E7%95%8C%E6%97%B6)

  • GMT格林尼治标准时间(英语:Greenwich Mean Time,GMT)是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时,因为本初子午线被定义为通过那里的经线。(摘自:https://zh.wikipedia.org/wiki/%E6%A0%BC%E6%9E%97%E5%B0%BC%E6%B2%BB%E6%A8%99%E6%BA%96%E6%99%82%E9%96%93)

  • CST北京时间,又名中国标准时间,是中国大陆的标准时间,比世界协调时快八小时(即UTC+8),与香港、澳门、台北、吉隆坡、新加坡等地的标准时间相同。

    北京时间并不是北京市的地方平太阳时间(东经116.4°),而是东经120°的地方平太阳时间,二者相差约14.5分钟[1]。北京时间由位于中国版图几何中心位置陕西临潼的中国科学院国家授时中心的9台铯原子钟和2台氢原子钟组通过精密比对和计算实现报时,并通过人造卫星与世界各国授时部门进行实时比对。(摘自:https://zh.wikipedia.org/wiki/%E5%8C%97%E4%BA%AC%E6%97%B6%E9%97%B4)

  • DST夏时制(英语:daylight time,英国与其他地区),又称夏令时、日光节约时间(英语:daylight saving time, DST,美国),是一种在夏季月份牺牲正常的日出时间,而将时间调快的做法。通常使用夏时制的地区,会在接近春季开始的时候,将时间调快一小时,并在秋季调回正常时间[1]。实际上,夏时制会造成在春季转换当日的睡眠时间减少一小时,而在秋季转换当日则会多出一小时的睡眠时间[2][3]。(摘自:https://zh.wikipedia.org/wiki/%E5%A4%8F%E6%97%B6%E5%88%B6)

  • 中国夏令时1986年4月,中国中央有关部门发出“在全国范围内实行夏时制的通知”,具体作法是:每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时,即将表针由2时拨至3时,夏令时开始;到九月中旬第一个星期日的凌晨2时整(北京夏令时),再将时钟拨回一小时,即将表针由2时拨至1时,夏令时结束。从1986年到1991年的六个年度,除1986年因是实行夏时制的第一年,从5月4日开始到9月14日结束外,其它年份均按规定的时段施行。在夏令时开始和结束前几天,新闻媒体均刊登有关部门的通告。1992年起,夏令时暂停实行。(摘自:https://baike.baidu.com/item/%E5%A4%8F%E4%BB%A4%E6%97%B6)

中国夏时制实施时间规定(夏令时)1935年至1951年,每年5月1日至9月30日。 1952年3月1日至10月31日。 1953年至1954年,每年4月1日至10月31日。 1955年至1956年,每年5月1日至9月30日。 1957年至1959年,每年4月1日至9月30日。 1960年至1961年,每年6月1日至9月30日。 1974年至1975年,每年4月1日至10月31日。 1979年7月1日至9月30日。 1986年至1991年,每年4月中旬的第一个星期日1时起至9月中旬的第一个星期日1时止。具体如下: 1986年4月13日至9月14日, 1987年4月12日至9月13日, 1988年4月10日至9月11日, 1989年4月16日至9月17日, 1990年4月15日至9月16日, 1991年4月14日至9月15日。

通过对比我们可以看到应用中的对应的用户生日"1988-07-29"刚好在中国的夏令时区间内,因为我们操作系统、数据库、JDK使用的都是 "Asia/Shanghai" 时区,应该不会错,通过上图中debug结果我们也证实了结果是没问题的。

继续往外排查业务层和接口层,定位到问题

项目使用的是spring boot提供rest接口返回json报文,使用spring 默认的Jackson框架解析。项目中有需要对外输出统一日期格式,对Jackson做了一下配置:

#jackson
#日期格式化
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

我们通过查看 JacksonProperties.java源码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.boot.autoconfigure.jackson;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(
  prefix = "spring.jackson"
)
public class JacksonProperties {
  private String dateFormat;
  private String jodaDateTimeFormat;
  private String propertyNamingStrategy;
  private Map serialization = new EnumMap(SerializationFeature.class);
  private Map deserialization = new EnumMap(DeserializationFeature.class);
  private Map mapper = new EnumMap(MapperFeature.class);
  private Map parser = new EnumMap(Feature.class);
  private Map generator = new EnumMap(com.fasterxml.jackson.core.JsonGenerator.Feature.class);
  private Include defaultPropertyInclusion;
  private TimeZone timeZone = null;
  private Locale locale;

  public JacksonProperties() {
  }

  public String getDateFormat() {
    return this.dateFormat;
  }

  public void setDateFormat(String dateFormat) {
    this.dateFormat = dateFormat;
  }

  public String getJodaDateTimeFormat() {
    return this.jodaDateTimeFormat;
  }

  public void setJodaDateTimeFormat(String jodaDataTimeFormat) {
    this.jodaDateTimeFormat = jodaDataTimeFormat;
  }

  public String getPropertyNamingStrategy() {
    return this.propertyNamingStrategy;
  }

  public void setPropertyNamingStrategy(String propertyNamingStrategy) {
    this.propertyNamingStrategy = propertyNamingStrategy;
  }

  public Map getSerialization() {
    return this.serialization;
  }

  public Map getDeserialization() {
    return this.deserialization;
  }

  public Map getMapper() {
    return this.mapper;
  }

  public Map getParser() {
    return this.parser;
  }

  public Map getGenerator() {
    return this.generator;
  }

  public Include getDefaultPropertyInclusion() {
    return this.defaultPropertyInclusion;
  }

  public void setDefaultPropertyInclusion(Include defaultPropertyInclusion) {
    this.defaultPropertyInclusion = defaultPropertyInclusion;
  }

  public TimeZone getTimeZone() {
    return this.timeZone;
  }

  public void setTimeZone(TimeZone timeZone) {
    this.timeZone = timeZone;
  }

  public Locale getLocale() {
    return this.locale;
  }

  public void setLocale(Locale locale) {
    this.locale = locale;
  }
}

得知 spring.jackson.time-zone 属性操作的就是java.util.TimeZone。于是我们通过一段测试代码模拟转换过程:

package com.test;

import java.sql.Date;
import java.util.TimeZone;

/**
 * @author alexpdh
 * @date 2019/07/17
 */
public class Test {

	public static void main(String[] args) {
		System.out.println("当前的默认时区为: " + TimeZone.getDefault().getID());
		Date date1 = Date.valueOf("1988-07-29");
		Date date2 = Date.valueOf("1983-07-29");
		System.out.println("在中国夏令时范围内的时间 date1=" + date1);
		System.out.println("正常东八区时间 date2=" + date2);
//		模拟 spring.jackson.time-zone=GMT+8 属性设置
		TimeZone zone = TimeZone.getTimeZone("GMT+8");
		TimeZone.setDefault(zone);
		System.out.println(TimeZone.getDefault().getID());
		Date date3 = date1;
		Date date4 = date2;
		System.out.println("转换后的在中国夏令时范围内的时间date3=" + date3);
		System.out.println("转换后的正常东八区时间 date4=" + date4);
	}
}

运行后输出结果:

当前的默认时区为: Asia/Shanghai
在中国夏令时范围内的时间 date1=1988-07-29
正常东八区时间 date2=1983-07-29
GMT+08:00
转换后的在中国夏令时范围内的时间date3=1988-07-28
转换后的正常东八区时间 date4=1983-07-29

从这里终于找到问题发生点了,从debug那张图我们看出了因为那个日期是在中国的夏令时区间内,要快一个小时,使用了UTC/GMT+0900的格式,而jackjson在将报文转换为json格式的时候使用的是UTC/GMT+0800的格式。也就是说我们将JDK时区为UTC/GMT+0900的"1988-07-29 00:00:00"这样的一个时间转换为了标准东八区的UTC/GMT+0800格式的时间,需要先调慢一个小时变成了"1988-07-28 23:00:00"。

bug解决

定位到问题解决就很简单了,只需要修改下设置:

#jackson
#日期格式化
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=Asia/Shanghai

保持时区一致问题得到解决。

到此,相信大家对“怎么排查因JDK导致接口输出日期格式的时间与预期时间不一致问题”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!


文章题目:怎么排查因JDK导致接口输出日期格式的时间与预期时间不一致问题
当前URL:http://chengdu.cdxwcx.cn/article/pjdjpo.html