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

发布时间:2021-11-15 16:49:46 作者:iii
来源:亿速云 阅读:105

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

bug描述

问题起源于同事在项目中新增一个统计用户生日明细的接口,其中一个用户在数据库中的生日日期是“1988-07-29”,然而通过rest接口得到该用户的生日日期却为 “1988-07-28”。

环境说明

开始bug排查之前,先说明下项目环境:

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

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

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

常用时间概念 UTC,GMT,CST,DST

中国夏时制实施时间规定(夏令时) 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<SerializationFeature, Boolean> serialization = new EnumMap(SerializationFeature.class);
  private Map<DeserializationFeature, Boolean> deserialization = new EnumMap(DeserializationFeature.class);
  private Map<MapperFeature, Boolean> mapper = new EnumMap(MapperFeature.class);
  private Map<Feature, Boolean> parser = new EnumMap(Feature.class);
  private Map<com.fasterxml.jackson.core.JsonGenerator.Feature, Boolean> 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<SerializationFeature, Boolean> getSerialization() {
    return this.serialization;
  }

  public Map<DeserializationFeature, Boolean> getDeserialization() {
    return this.deserialization;
  }

  public Map<MapperFeature, Boolean> getMapper() {
    return this.mapper;
  }

  public Map<Feature, Boolean> getParser() {
    return this.parser;
  }

  public Map<com.fasterxml.jackson.core.JsonGenerator.Feature, Boolean> 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导致接口输出日期格式的时间与预期时间不一致问题”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

推荐阅读:
  1. 排查CentOS 7中chronyc sources 时间无法同步问题
  2. PHP中date()函数输出的时间与Linux不一致应该怎么处理

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

jdk

上一篇:rexray在CentOS上不能创建ceph rbd的docker volume问题怎么办

下一篇:KubeSphere基于Ingress-Nginx怎样实现灰度发布

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》