夏令时踩坑记录二

发布时间 2023-05-25 11:35:27作者: zeng1994

一、现象描述

第三方数据上传,上传上来发现有一个人的出生日变成了 1991-07-15 23:00:00。

这个时间明显不对,理论上这个的生日是1991年7月16号。

曾经踩过夏令时的坑, 一看这个就知道应该是夏令时时区转换导致时间相差一个小时。

二、问题分析与解决

2.1 数据流转说明

欲分析是哪里出现了时区转换,那么我们得把数据的流转搞清楚。

这个数据流转如下:第三方系统(java应用) --> api接口服务(java应用) --> 数据存储服务(java应用) --> 数据库

2.2 各个环节分析

2.2.1 第三方系统 --> api接口服务

  • 这个环节中,都是日期字符串传输,不会发生时区转换

2.2.2 api接口服务 --> 数据存储服务

  • api接口服务内部用Date类型来接收的日期字符串(这里要考虑时区问题)
  • api接口再将Data转成了字符串,传输到了数据存储服务(该里也要考虑时区问题)

2.2.3 数据存储服务 --> 数据库

  • 在数据存储服务里面,看到参数是用Java的Date来接收的(这里也要考虑时区问题)
  • 数据库用的时区是GMT+8

2.3 问题分析

2.3.1 发现疑点

从上面的数据流转各个环节分析,逐个系统排查。api接口服务排查,情况如下:

  • 字符串-->Date-->字符串,这两步用了同一个时区,不会发生转换
  • 通过该系统打印的日志,发现接口服务往数据存储服务发的日期字符串还是 1991-07-16
  • 通过分析发现api接口服务不会发生时区转换

数据存储服务排查,情况如下:

  • 发现用的是FastJson进行了的JSON反序列化
  • 通过打印的日志,发现输出了一个时间的毫秒值,值为679590000000L
  • 这个值就有意思了,通过如下代码验证,发现在系统时间为GMT+8时,该毫秒值对应日期为1991-07-15 23:00:00,参考下面代码
    
public class TestDate {
    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                .format(new Date(679590000000L)));
        // 输出结果为 1991-07-15 23:00:00
    }
}
  • 通过这个分析,那么就发现了这个日期在FastJson转换时,用了Asia/Shanghai;上面代码中把时区设置为上海时,输出的结果就是1991-07-16

2.3.2 分析原因

通过上一章节分析,知道了是数据存储服务中FastJson用了上海时区导致了时区转换。那么需要分析为何数据存储服务里面会有2个不同的时区。SpringBoot项目,直接从服务启动类看,服务是指定了时区的;代码如下

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        // fastjson开启安全模式
        ParserConfig.getGlobalInstance().setSafeMode(true);
        SpringApplication.run(Application.class, args);
    }

    @PostConstruct
    public void init () {
        // 设置系统时区
        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("GMT+8")));
    }
}

简单分析:

  1. 第一时间看该代码,好像没啥问题,init方法里面指定了时区统一用GMT+8
  2. 从git提交记录发现了端倪,发现这个fastjson开启安全模式的代码是近期加的

猜想与验证:突然灵光一闪:这行代码运行是在init方法前的,如果此时fastjson获取时区的话,那么就会用系统时区;就有可能用了 Asia/Shanghai。因此,直接把设置系统时区的代码放到main方法第一行去,验证问题是否解决?代码如下:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
         // 设置系统时区
        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("GMT+8")));
        // fastjson开启安全模式
        ParserConfig.getGlobalInstance().setSafeMode(true);
        SpringApplication.run(Application.class, args);
    }

}

发现问题解决了!!!从源码找答案:分析fastJson的源码,发现JSON类被加载就会获取时区


public abstract class JSON implements JSONStreamAware, JSONAware {
    public static TimeZone         defaultTimeZone      = TimeZone.getDefault();
    public static Locale           defaultLocale        = Locale.getDefault();
    // ...其他源码就不贴了
} 

   

因此是fastjson开启安全模式的代码导致JSON类被提取加载,从而读取了系统时区并设置到静态变量里去了

三、解决方案

3.1 临时方案

在不更新服务的情况下,线上启动数据存储java服务时通过启动命令指定为东八区

-Duser.timezone="GMT+8" 

3.2 永久解决方案

修改代码,把指定时区的代码放在第一行,参考下面代码

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        // 把时区设置提到第一行,防止类加载导致时区设置失败;
        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("GMT+8")));
        // ParserConfig会导致JSON类提取加载,会执行JSON.defaultTimeZone的初始化,
        // 所以必须要先把系统时区设置正确
        ParserConfig.getGlobalInstance().setSafeMode(true);
        SpringApplication.run(Application.class, args);
    }

}