Java 常用类型系列(五)时区处理总结
本文介绍时区处理的两种方式。时区涉及的接口及实现类如下:
(上图简化掉了 Serializable
接口、Comparable
接口及 FunctionalInterface
注解)
推荐方式
ZoneId
时区的处理是新版日期与时间 API 新增的重要功能,且 API 被极大简化。新的 java.time.ZoneId
类是老版本 java.util.TimeZone
类的替代品。它的设计目标就是要让用户无需为时区处理的复杂和繁琐而操心,比如处理夏令时(DST)问题。
每个特定的 ZoneId
对象都有一个地区 ID 标识。地区 ID 格式为“{区域}/{城市}
”,这些地区集合的设定都由 IANA 的时区数据库提供。可以输出如下:
1 | ZoneId.getAvailableZoneIds().forEach(System.out::println); |
ZoneId
的静态工厂方法构造如下:
1 | // 获取服务器所在时区的 ZoneId,例如 Asia/Shanghai 为 UTC+8 |
一旦得到一个 ZoneId
对象,就可以与 LocalDate
、LocalDateTime
、Instant
对象整合起来,构造一个 ZonedDateTime
实例,它代表了相对于指定时区的时间点。
ZonedDateTime
https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html
A date-time with a time-zone in the ISO-8601 calendar system, such as
2007-12-03T10:15:30+01:00 Europe/Paris
.
Java 8 中 ZonedDateTime
基于 ISO-8601 实现,参考这里。
底层实现
ZonedDateTime
的底层实现如下:
1 | public final class ZonedDateTime |
ZonedDateTime
的实例如下图:
图中可见,原本 LocalDateTime
对象作为一个本地日期与时间,是不包含时区信息的,即没有时区概念。而在结合了 ZoneId
构造成一个 ZonedDateTime
实例之后,才有了时区概念。它代表了相对于指定时区的时间点。
使用方式
1 | // 1970-01-01 |
LocalDateTime 与 Instant 互转
通过 ZoneId
可以将 LocalDateTime
和 Instant
进行互转,公式为 UTC + 时区差(东正西负)= 本地时间。
LocalDateTime
> Instant
:
1 | // 东八区的 1970-01-01T00:00,等于 UTC+0 的 1969-12-31T16:00:00Z |
Instant
> LocalDateTime
:
1 | // 1970-01-01T00:00 |
不推荐方式
ZoneOffset
另一种比较通用的表达时区的方式是利用当前时区和 UTC/格林尼治的固定偏差。可以使用 ZoneOffset
类,它是 ZoneId
的一个子类,表示的是当前时间和 UTC 的偏差:
1 | ZoneOffset newYorkOffset = ZoneOffset.of("-05:00"); |
OffsetDateTime
底层实现
ZoneOffset
类可用于构造 OffsetDateTime
实例。OffsetDateTime
的底层实现如下:
1 | public final class OffsetDateTime |
使用方式
1 | // 1970-01-01T00:00-05:00 |
“-05:00” 的偏差实际上对应的是美国东部标准时间。注意,使用这种方式定义的 ZoneOffset
并未考虑任何夏令时的影响,所以在大多数情况下,不推荐使用。
常见问题
java.sql.Timestamp
有时开发会使用 java.sql.Timestamp
作为 PO 实体类的时间字段,java.sql.Timestamp
底层实现使用格里历(公历),并使用服务器所在时区(即本地时区),并受该时区影响。
这里看一段代码,以 2021-01-04 00:00:00 为例演示转换过程:
1 | LocalDateTime localDateTime = LocalDateTime.parse( |
这里试验两个时区:
Europe/London (UTC) | Asia/Shanghai (UTC+8) | |
---|---|---|
ZoneId.systemDefault() |
Europe/London (UTC) | Asia/Shanghai (UTC+8) |
TimeZone.getDefaultRef() |
Europe/London (UTC) | Asia/Shanghai (UTC+8) |
下面分别看下 java.sql.Timestamp
两个 API 会有什么问题:
#valueOf(LocalDateTime)
转换过程:本地时间 > 系统时区的时间 > UTC-0 时区的时间戳
Europe/London (UTC) | Asia/Shanghai (UTC+8) |
---|---|
2021-01-04T00:00 (LocalDateTime ) → 2021-01-04T00:00:00.000Z / 1609718400000 ( Timestamp ) |
2021-01-04T00:00 (LocalDateTime ) → 2021-01-04T00:00:00.000+0800 / 1609689600000 ( Timestamp ) |
可见,由于 LocalDateTime
本身不含时区信息,在经由 Timestamp#valueOf(LocalDateTime)
转换时,源码中使用了 TimeZone.getDefaultRef()
并受系统默认时区的影响,导致结果前后不一致。
1 | /** |
#from(Instant)
转换过程:本地时间 > 指定时区的时间 > UTC-0 时区的时间戳
Europe/London (UTC) | Asia/Shanghai (UTC+8) |
---|---|
2021-01-04T00:00 (LocalDateTime ) → 2021-01-04T00:00+07:00[Asia/Jakarta] ( ZonedDateTime ) → 2021-01-03T17:00:00Z / 1609693200 ( Instant ) →2021-01-03T17:00:00.000Z / 1609693200000( Timestamp ) |
2021-01-04T00:00 (LocalDateTime ) → 2021-01-04T00:00+07:00[Asia/Jakarta] ( ZonedDateTime ) → 2021-01-03T17:00:00Z / 1609693200 ( Instant ) →2021-01-04T01:00:00.000+0800 / 1609693200000 ( Timestamp ) |
这里看似结果没有问题,Instant
和 Timestamp
对象在不同时区下都是相同时间戳。
但有一种场景,就是应用服务器与数据库的时区不一致导致的问题。假如应用服务器时区为 Asia/Shanghai (UTC+8)
,数据库时区为 Europe/London (UTC)
,当把上表 Timestamp
对象保存到 MySQL 数据库的 datetime
字段时,如果未经时区转换,会导致错误结果。
这里参考 mysql-connector-java-5.1.42.jar 源码如下,重点看 java.sql.PreparedStatement#setTimestamp
的方法实现,其使用了 SimpleDateFormat
将 Timestamp
对象格式化成字符串,如果未经时区转换,结果如下表,导致前后不一致:
格式化前 | 格式化后 |
---|---|
2021-01-03T17:00:00.000Z | 2021-01-03 17:00:00 |
2021-01-04T01:00:00.000+0800 | 2021-01-04 01:00:00 |
1 | /** |
上述方法内部调用了 com.mysql.jdbc.TimeUtil#changeTimezone
方法,源码如下。
1 | /** |
如果 JDBC 连接参数未配置 useTimezone=true
(默认值 false
),会导致目标时区转换失效,从而产生上述问题。而如果开启之后,不管应用服务器设置什么时区,都能保证正确转换为数据库目标时区的时间值,反之亦然(数据库 -> 应用服务器)。这里给两个例子,如下表:
时区转换前 | 时区转换后 | |
---|---|---|
UTC+2 | 2021-01-03T19:00:00.000+0200 / 1609693200 | 2021-01-03T17:00:00.000Z / 1609693200 |
UTC+8 | 2021-01-04T01:00:00.000+0800 / 1609693200 | 2021-01-03T17:00:00.000Z / 1609693200 |
参考:
不指定时区会踩坑:MySQL JDBC 8.0.22 驱动升级遇到的 Bug 分析
参考
时区数据库:
《这个重要开源项目全靠一位低调的 “怪老头” 维护!他和比尔盖茨一样撑起了计算机世界》
时区设置背后有一组大量关于全球许多代表性地点时间历史信息的代码和数据,这些代码和数据被称为时区数据库(即 tz、tzdata 或 zoneinfo),该数据库会定期进行更新以反映各政治实体对时区边界、UTC 差值和夏令时规则的更改。对 tz 的更新遵循 BCP 175 流程进行管理。
尽管大多数计算机用户从未听说过时区数据库,但 tz 数据库对全世界的计算机非常重要。所有基于 Linux 和 Mac 的计算机都是从一个极其重要的数据库(时区数据库)中提取时区。目前,使用该数据库的项目包括:the GNU C Library (used in GNU/Linux), Android, FreeBSD, NetBSD, OpenBSD, Chromium OS, Cygwin, MariaDB, MINIX, MySQL, webOS, AIX, BlackBerry 10, iOS, macOS, Microsoft Windows, OpenVMS, Oracle Database, Oracle Solaris 等。
tz 数据库背后,一个人在维护
tz 数据库由 David Olson 创立,收集了自 1970 年以来被广泛认可的民用时钟的时区信息。2011 年,互联网域名与数字地址分配机构 ICANN 接管了这个被全球电脑和网站广泛使用的时区数据库,该机构通常只赞助对互联网发展非常重要的项目,
现在,具体的维护工作由互联网分配号码管理局(Internet Assigned Numbers Authority, IANA)负责。Paul Eggert 是时区数据库的项目负责人,该职位被称为 TZ 协调员。
UTC:
闰秒终于要被取消了!
2022 年 11 月 20 日消息,负责协调世界时的国际计量局(BIPM)表示,科学家和政府代表 18 日在法国举行的一次会议上投票决定到 2035 年取消闰秒。BIPM 时间部门负责人帕特里齐亚·塔维拉表示,这项“历史性决定”将允许“秒数连续流动,而不会出现目前由不规则闰秒造成的不连续性。”
更多阅读:
正式宣布取消!能让 Linus 本人同谷歌微软达成一致的,只有它了!
让大厂抓狂的“额外一秒”:谷歌、微软、Meta 和亚马逊纷纷提议放弃
当闰秒发生时,就需要通过网络时间协议 NTP (Network time protocol) 来进行时间同步,NTP 服务器会一级一级地下发闰秒事件通知直到最边缘的 NTP 服务器,然后 NTP 服务器就会把闰秒通知发给客户端的操作系统,由操作系统来处理闰秒通知。
如果你的计算机系统没有开启 NTP 服务,那么导致的问题就是你的计算机上的机器时间就会比世界时间慢 1 秒。
如果开了 NTP 服务的话,就需要操作系统来处理这个闰秒。
时间协议:
- https://en.wikipedia.org/wiki/Time_Protocol
- https://en.wikipedia.org/wiki/Network_Time_Protocol
- https://en.wikipedia.org/wiki/Network_Time_Protocol#SNTP
- 《每天学一个 Linux 命令(96):
ntpdate
》
其它:
- 《Java 8 实战》
- Java SE Docs
- 《八十天环游地球》【法】儒勒·凡尔纳
- 《Java 时区数据库与 IANA 数据》