Java 中的时区理解和处理

本文内容:
1,时区了解一下
2,产生时区的原因
3,常用时间类,哪些是有会导致时区问题
4,开发中如何避免产生时区问题

1. 时区了解一下

时区的概念:
之所以有时区,是因为住在地球上不同地方的人呢看到的太阳升起的时间是不一样的,我们假设北京人民早上8:00看到了太阳刚刚升起,而此刻欧洲人民还在夜里,欧洲要再过7个小时才能看到太阳升起,所以,此刻,欧洲人民的手表上显示的是凌晨1:00。
如果强迫欧洲人民使用北京时间,那他们每天看到太阳升起是在下午3:00。
也就是说,东八区的北京人民的手表上显示的8:00 和 东一区欧洲人民手表上显示的1:00 是相同的时刻:

"2014-10-14 08:00 +8:00" = "2014-10-14 01:00 +1:00"

这就是本地时间的概念

但是,在计算机中,如果用本地时间来存储日期和时间,在遇到时区转换的问题上,就会非常麻烦,有很容易出问题,尤其美国人还在采用夏令时(时区会变动)。

所以,我们需要引入“绝对时间”的概念,绝对时间不是年月日格式,而是以秒来计算,当前时间是指从一个基准时间(1970-1-1 00: 00:00 +0:00)到现在的秒数,用一个整数表示。
当用绝对时间表示时间时,无论服务器在哪个时区,任意时刻,生成的时间值都是相等的。
所有编程语言都提供了方法来生成时间戳,Java 和JavaScript 输出以毫秒计算的Long 型整数,Python输出标准的Unix 的以秒级计算的时间戳。

实际上,操作系统内部的计时器也是这个标准的时间戳,只有在显示给用户的时候,才转换晨字符串格式的本地时间。

GMT & UTC & UNIX 时间戳

GMT(Greenwich Mean Time),即格林威治标准时,是东西经零度的地方。
UTC 即协调世界时,在时刻上接近于GMT 的时间。
UTC与GMT基本上等同,误差不超过0.9秒。

地球自西向东旋转,东边比西边先看到太阳,东边的时间也比西边的早。为了统一世界的时间,1884年的国际经度会议规规定将全球划分为24个时区(东、西各12个时区)。规定英国(格林尼治天文台旧址)为零时区(GMT+00),东1-12区,西1-12区,中国北京处于东8区(GMT+08)。

若英国时间为6点整,则GMT时间为6点整,则北京时间为14点整。

计算机中的UNIX时间戳,是以GMT/UTC时间「1970-01-01T00:00:00」为起点,到具体时间的秒数,不考虑闰秒。这么做当然是为了简化计算机对时间操作的复杂度。

比如我的电脑现在的系统时间为2015年2月27日15点43分0秒,因为我的电脑默认时区为东8区,则UNIX时间戳为1425022980秒. 则0时区的时间为2015年2月27日7点43分0秒.

人们将地球人为的分为24等份,每一等份为一个时区,每时区横跨经度15度,时间正好为1小时。往西一个时区,则减去一小时;往东一个时区,则加上一小时。中国在东经120度上,(东经120°-东经0°)所得度数再除以15,即得8。

所以中国使用的是东八区的时间

UTC + (+0800) = 本地(北京)时间

在计算机中看到的UTC时间都是从(1970年01月01日 0:00:00)开始计算秒数的。所看到的UTC时间那就是从1970年这个时间点起到具体时间共有多少秒。 这个秒数就是Unix时间戳

我们知道我们的时间是从1970年1月1日0时开始计算的,至于为什么要使用这个时间为开始时间?

网上有很多乱七八糟的解释,但是答案却就是他们解释的那样:因为当时只有UNIX 操作系统,并且是32位,而很多编程语言都是起源于UNIX 操作系统,当时是因为32位能表示的最大镇整数值:2147483647 , 1年365天的总秒数是 31536000, 所以2147483647/31536000 = 68.1,也就是说32位能表示的最长时间是68年,也就是很快就会达到操作系统能表示的最大时间,所以最终UNIX 考虑到计算机产生的年代和使用的时限,综合取了1970年1月1日作为UNIX TIME的纪元时间(开始时间)。
所以当我们执行如下代码,可以知道 Date 对象里设置了0时间即开始时间就是1970年1月1日:

System.out.println(new Date(0));
output:Thu Jan 01 08:00:00 CST 1970
时区举例

全球分为24个时区,相邻时区时间相差1个小时。

比如北京处于东八时区,东京处于东九时区,北京时间比东京时间晚1个小时,而英国伦敦时间比北京晚7个小时(英国采用夏令时时,8月英国处于夏令时)。
比如此刻北京时间是2017年8月24日11:17:10,则东京时间是2017年8月24日12:17:10,伦敦时间是2017年8月24日4:17:10。

既然Date 对象中存放的是当前时刻距离格林威治时间( GMT)1970年1月1日0点 所经过的毫秒数,如果此刻在伦敦、北京、东京有三个程序员同时执行如下语句:

Date date = new Date();

那这三位程序员得到的 Date 对象里存放的毫秒数相同吗?还是北京的比东京的小3600000(北京时间比东京时间晚1小时,1小时为3600秒即3600000毫秒)?
实际上这个3个Date 对象中的毫秒数是完全一样的

确切的说,Date对象里存的是自格林威治时间( GMT)1970年1月1日0点至Date对象所表示时刻所经过的毫秒数。所以,如果某一时刻遍布于世界各地的程序员同时执行new Date语句,这些Date对象所存的毫秒数是完全一样的。
也就是说,Date里存放的毫秒数是与时区无关的。

如果上述3个程序员调用 new Date() 那一刻的时间是北京时间2017年8月24日11:17:10,他们继续调用:

System.out.println(date);

那么
北京的程序员将会打印出2017年8月24日11:17:10
东京的程序员会打印出2017年8月24日12:17:10
伦敦的程序员会打印出2017年8月24日4:17:10

既然Date对象只存了一个毫秒数,为什么这3个毫秒数完全相同的Date对象,可以打印出不同的时间呢?

这是因为Sysytem.out.println函数在打印时间时,会取操作系统当前所设置的时区,然后根据这个时区将同毫秒数解释成该时区的时间。

当然我们也可以手动设置时区,以将同一个Date对象按不同的时区输出。
可以做如下实验验证:

Date date = new Date(1503544630000L);  // 对应的北京时间是2017-08-24 11:17:10
 
SimpleDateFormat bjSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");     // 北京
bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));  // 设置北京时区
 
SimpleDateFormat tokyoSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  // 东京
tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));  // 设置东京时区
 
SimpleDateFormat londonSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 伦敦
londonSdf.setTimeZone(TimeZone.getTimeZone("Europe/London"));  // 设置伦敦时区
 
System.out.println("毫秒数:" + date.getTime() + ", 北京时间:" + bjSdf.format(date));
System.out.println("毫秒数:" + date.getTime() + ", 东京时间:" + tokyoSdf.format(date));
System.out.println("毫秒数:" + date.getTime() + ", 伦敦时间:" + londonSdf.format(date));

输出为:
毫秒数:1503544630000, 北京时间:2017-08-24 11:17:10
毫秒数:1503544630000, 东京时间:2017-08-24 12:17:10
毫秒数:1503544630000, 伦敦时间:2017-08-24 04:17:10

可以看出,同一个Date 对象,按不同的时区来格式化,将得到不同时区的时间,所以Date 对象中保存的毫秒数到输出的时间(年月日十分秒)是由时区决定输出对应的时间。

 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  // 东京
 System.out.println(df.getTimeZone().getID());

输出结果:
Asia/Shanghai

说明SimpleDateFormat 对象默认的时区就是北京东八区时区。

从字符串中读取时间

有时候我们需要从一个字符串中解析得到一个Date 对象或者时间戳之类的需求,那么问题来了,那这个时间到底是指哪个时区的时间呢?
所以从字符串中解析时间正确的做法是:指定时区来解析

String timeStr = "2017-8-24 11:17:10"; // 字面时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); // 设置北京时区
Date d = sdf.parse(timeStr);
System.out.println(sdf.format(d) + ", " + d.getTime());

输出为:
2017-08-24 11:17:10, 1503544630000
将字符串表示的时间转换成另一个时区的时间字符串

1.将字符串时间按原时区转换为Date 对象

  1. 将Date 对象转换为目标时区的时间

String timeStr = "2017-8-24 11:17:10"; // 字面时间
SimpleDateFormat bjSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
Date date = bjSdf.parse(timeStr);  // 将字符串时间按北京时间解析成Date对象
 
SimpleDateFormat tokyoSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  // 东京
tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));  // 设置东京时区
System.out.println("北京时间: " + timeStr +"对应的东京时间为:"  + tokyoSdf.format(date));

输出为:
北京时间:2017-8-24 11:17:10对应的东京时间为:2017-08-24 12:17:10

格林威治时间( GMT)1970年1月1日0点 ???
Java中处理时区使用的是TimeZone类,通过TimeZone.getTimeZone(String id)方法可以获取到指定时区的TimeZone实例,通过TimeZone实例可以获取到相对于GMT标准时间的偏移量。该方法的参数ID可以是GMT、 UTC、CST等时区,也可以是城市名:
TimeZone.getTimeZone("Asia/Shanghai") = TimeZone.getTimeZone("GMT+8")

为什么北京时间的时区是Asia/Shanghai ?

中国的国土横跨了五个地理时区(东五区至东九区),其中大部分处于东六区、东七区和东八区 [2],在民国二十八年(1939年),南京政府批准将全国划为五个标准时区:
中原时区(GMT+8)
陇蜀时区(GMT+7)
新藏时区(GMT+6)
昆仑时区(GMT+5:30)
长白时区 (GMT+8:30)
1949 年中华人民共和国成立后废止了民国政府的五分法,改为全国统一的「北京时间」,即 UTC+8。而民国政府在台湾依然延续使用五分法,使用中原时区作为标准时间,电视及广播亦使用「中原标准时间」进行播报,直到 2000 年左右才开始使用「NST - 台湾标准时间」代替,而中国时区划分方式不变。

在JDK诞生之前,国际标准时区就没有Asia/Beijing,只有Asia/Shanghai或Asia/Chongqing,所以就选择了Asia/Shanghai 作为中国标准时区。

产生时区的原因

比如我们在开发中在自己的机器(默认时区就是:东八区时区)上获取当前时间,然后将获取时间的时间戳传给后端入库,在需要格式化显示时间的地方用SimpleDateFormat进行解析,在我们本机解析出来肯定没问题,因为SimpleDateFormat 也会使用本机默认的时区进行解析;但是如果到了我们的dev, uat, prod 环境的服务器上运行我们的应用时,在前端用户电脑上获取的时间的时间戳传入后端服务器上去格式化解析,就会出现如下问题:
如果服务器没有设置任何时区,即服务器的时区为世界标准时间,简称UTC 时间不属于任何时区,UTC 即为0时区时间,默认比北京少8个小时

服务器UTC时区

这就是服务器上的时区
中国的时区应该式CST(China Standard Time 中国标准时间), 那如果想要把服务器时区改为:
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
中国时区

所以,不管我们线上服务器有没有设置成CST 时区或者其他时区,我们在代码中解析时间的时间,都最好显式的指定时区去解析格式化时间,而不能只看在本机不指定时区的是没问题的,因为在本机 SimpleDateFormat 默认使用的是我们本机的 东八区时区(CST 时区)

这就是因为各个成员机器的时区设置不同,导致解析结果不同。

常用时间类,哪些是有会导致时区问题
  • Date
Date date = new Date();
System.out.println(date + ", " + date.getTime());
输出如下:
Thu Aug 24 10:48:05 CST 2017, 1503542885955

Date 对象里其实是存了long型的时间戳数据,Date对象中记录的是当前时刻距离1970年1月1日0点所经过的毫秒数,调用getTime()方法就可以返回这个毫秒数。
而输出的年月日十分秒其实是通过这个毫秒数计算出来的.

  • SimpleDateFormat
    通常Java 中是使用SimpleDateFormat来解析时间,格式化时间,但是这个类默认会使用本机的时区来解析时间,所以比如在服务器上时区不是中国标准时区,解析时间自然会有问题。
Date date = new Date();  // 对应的北京时间是2019-04-01 11:17:10
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 中国北京
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));  // 设置中国标准时区
System.out.println("毫秒数:" + date.getTime() + ", 北京时间:" + sdf.format(date));

输出:
毫秒数:1503544630000, 北京时间: 2019-04-01 11:17:10
开发中如何避免产生时区问题

在平时开发中,经常会遇到时间时区问题,比如:在本地获取时间,时间转换为时间戳都没有问题,但是到了dev, qa ,prod环境就不正确了;大部分都是时区导致问题。

  • 正确的时间存储方式
    基于“数据的存储和显示相分离”的设计原则,我们只要把表示绝对时间的时间戳(Long型)存储数据库,在显示的时候根据用户设置的时区格式化为正确的字符串。

  • 数据库时间存储时间格式举例

数据库:update_time TIMESTAMP(0)  WITHOUT  TIME ZONE NOT NULL DEFAULT NOW()
Java:  private Date updateTime;
s1.setUpdateTime(new Date());
存储格式:2019-03-27 11:35:40

createTime  BIGINT NULL
private Long createTime 
存储格式:1554043669000

在国际化的项目中需要处理的日期时间问题主要有两点:
1、日期时间的国际化格式问题处理;
2、日期时间的时区问题处理,这两个问题要区分开,不要弄混了
日期时间国际化化格式处理

日期时间国际化化格式处理
对应的关键词:Locale
日期时间的国际化格式指的是在不同的国家和地区对日期时间的显示方式不同,主要通过不同国家地区不同的语言习惯,对同一个实现的呈现方式不同。在java中需要结合Locale类进行处理:

public static void main(String[] args) {  
        Date date = new Date();  
        Locale locale = Locale.CHINA;  
        DateFormat shortDf = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM, locale);  
        System.out.println("中国格式:"+shortDf.format(date));  
   
        locale = Locale.ENGLISH;  
        shortDf = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM, locale);  
        System.out.println("英国格式:"+shortDf.format(date));  
}  
   
输出:
中国格式:2017-10-12 10:29:44  
英国格式:Oct 12, 2017 10:29:44 AM  

日期时间国际化化时区处理
对应的关键词:TimeZone

日期时间的时区问题,指的是在同一时刻,地球上的各个地区的日期时间不同。全球划分为24个时区,每个相邻时区时间相差一个小时(中国为了方便统一,虽然跨越5个时区,但都使用同一个时区时间),也就是说在同一时刻,全球同一时刻对应的当地时间的小时数有可能是0-23点之间的一个值。这里拿中国上海和英国伦敦举例:


public static void main(String[] args) {  
        Date date = new Date();  
        Locale locale = Locale.CHINA;  
        DateFormat shortDf = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM, locale);  
        shortDf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));//Asia/Chongqing  
        System.out.println(TimeZone.getDefault().getID());  
        System.out.println("中国当前日期时间:" + shortDf.format(date));  
   
        locale = Locale.ENGLISH;  
        shortDf = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM, locale);  
        shortDf.setTimeZone(TimeZone.getTimeZone("Europe/London"));  
        System.out.println("英国当前日期时间:"+shortDf.format(date));  
    } 

输出:
中国当前日期时间:2017-10-12 10:55:55  
英国当前日期时间:Oct 12, 2017 3:55:55 AM 

说明同一时刻,中国上海和英国伦敦相差7个小时,也就是相差7个时区。
国际时间处理请参考

时间处理demo:

 private String getTimeEnd(WxPaymentNotificationRequest request) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(TIME_FORMAT);
        LocalDateTime localDateTime = LocalDateTime.parse(request.getTimeEnd(), formatter);
        return String.valueOf(localDateTime.atZone(ZoneId.of(TIME_ZONE)).toInstant().toEpochMilli());
      // 最后处理的结果是时间戳
    }

你可能感兴趣的:(Java 中的时区理解和处理)