# Java 时区问题

# 1. Java 时区相关

# 时间格式

UTC 是以原子时计时,更加精准,适应现代社会的精确计时。不过一般使用不需要精确到秒时,视为等同。GMT 是前世界标准时,UTC 是现世界标准时。

TIP

每年格林尼治天文台会发调时信息,基于 UTC 。

GMT 和 UTC 可以视为几乎是等同的,UTC 更精准,有闰秒的概念。

  • 世界标准时间 UTC:2010-10-12T15:24:22Z or 2010-10-12 15:24:22Z

    其中 T 表示时分秒的开始(或者日期与时间的间隔)Z 表示这是一个世界标准时间

  • 本地时间(也叫不含时区信息的时间)2010-10-12T15:24:22

    本地时间的末尾没有 Z。对于不同时区的人而言,如果两者交流间使用的是本地时间,那么会引发歧义。

  • 含有时区的时间:2017-12-13T09:47:07.153+08:00[Asia/Shanghai]"

    +08:00 表示该时间是由世界标准时间加了 8 个小时得到的,[Asia/Shanghai] 表示时区。

    由于 +08:00 的存在,所以将表示时区的 [Asia/Shanghai] 省略掉,也不会导致时间概念上的歧义。

# 表示时间相关的类

在 Java 编码中,表示时间的类主要有个:String、Instant、LocalDateTime、ZonedDateTime 。

String 是字符串形式的时间,Instant 是时间戳,LocalDateTime 是不含时区信息的时间,ZonedDateTime 是含有时区信息的时间。

  • LocalDateTime

    符合格式的 String 可以直接转换为 LocalDateTime 。

    String str = LocalDateTime.parse(
        "2019-12-15 10:10:10", 
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    
    System.out.println(str);
    

    LocalDateTime 字面意思是本地时间,实际上它可以理解为不含时区信息的时间,只储存了年月日时分秒,要表达是哪里的时间需要时区解释,即,这是一个逻辑上有歧义的时间。

  • Instant 与 ZonedDateTime

    Instant 是时间戳,是指世界标准时格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(即北京时间 1970 年 01 月 01 日 08 时 00 分 00 秒)起至现在的总秒数。

    注意,由此可见 Instant 本身已经携带了时区信息,也就是 0 时区。当然,这只是默认值,有需要的话你可以指定的。

    ZonedDateTime 是含有时区信息的时间,可以理解为它是 Instant 的格式化对象。

    JDK 8 以前的时区是用 TimeZone,TimeZone ID 是在 java 里 ZoneInfoFile 类加载的。在 jvm 初始化的时候,会读取 jdk 安装目录下的 ${java.home}/jre/lib/tzdb.dat,放到其成员变量为 zones 的 ConcurrentHashMap 里。当调用 TimeZone.getTimeZone(id) 方法时,会用 id 到这个 map 里进行匹配获取到指定 id 的时区。其中TimeZone.getTimeZone("Asia/Shanghai")TimeZone.getTimeZone("GMT+8") 是相同的,可以相互替换使用。

    System.out.println(ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toInstant());
    System.out.println(ZonedDateTime.ofInstant(Instant.now(), "Australia/Darwin").toInstant());
    

    相同的 Instant,在不同的时区有不同的展示时间,所以在用 Instant 构造 ZonedDateTime 的时候需要传入时区;ZonedDateTime 可以直接转化为 Instant ,并且不同的 ZonedDateTime 可能会生成同样的 Instant 。

# 2. 时区转换

用户输入的 String 类型的时间是没有时区信息的,需要人为指定时区再解析。

解析的步骤分 2 步: 先确定用户时区

  1. 把用户输入的时间转化为世界标准时间;Instant.parse("2010-10-12T15:24:22Z")

  2. 再把世界标准时间转为需要的时区的时间。ZonedDateTime.ofInstant(instant,ZoneId.systemDefault());

# 3. 不同地区的服务器统一时间的解决方案

首先后端封装一个接口后获取服务器相对 GMT(格林尼治标准时间)时间的偏移量:

TimeZone zone = TimeZone.getDefault();
System.out.println(zone.getRawOffset());

这段代码放在不同时区的服务器上执行结果会不同(前提是服务器的时区设置跟本地时区一致)。如果在泰国执行结果为 25200000ms ,换算成小时为 7 ,说明泰国的时区的偏移量相对于 GMT 标准时间相差 7 小时。下文简称“时区偏移量”。

前端首先调用该接口获取服务器的时区偏移量,再在浏览器上获取本地的时区偏移量,计算出两个偏移量的差值。本地浏览器上获取当前的时间戳,减去上一步计算出来的差值即可得到服务器这个时间的时间戳,把这个时间戳传给后端 再转换成时间,就是服务器对应的时间,存入数据库即可。

前端:

// 服务的时区偏移量,通过接口获得,注意换成负值
var serveroffset=-25200000;

var d = new Date();

// 获取本地浏览器的时区偏移量
var localOffset = d.getTimezoneOffset() * 60000;

// 得到本地和偏移量的差值
var deffoffset=localOffset-(serveroffset);

// 获取本地浏览器时间戳
var localTime = d.getTime();

// 计算出传到服务器的时间戳
var servertime=localTime+deffoffset;

通过上述方式,可以实现服务器全球各地部署,系统都可以正常使用。