# Java 时区问题
# 1. Java 时区相关
# 时间格式
UTC 是以原子时计时,更加精准,适应现代社会的精确计时。不过一般使用不需要精确到秒时,视为等同。GMT 是前世界标准时,UTC 是现世界标准时。
TIP
每年格林尼治天文台会发调时信息,基于 UTC 。
GMT 和 UTC 可以视为几乎是等同的,UTC 更精准,有闰秒的概念。
世界标准时间 UTC:
2010-10-12T15:24:22Z
or2010-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 步: 先确定用户时区
把用户输入的时间转化为世界标准时间;Instant.parse("2010-10-12T15:24:22Z")
再把世界标准时间转为需要的时区的时间。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;
通过上述方式,可以实现服务器全球各地部署,系统都可以正常使用。
← 契约式编程与防御式编程 异常的作用 →