java.sql.Date 和时区不同

java.sql.Date and Time Timezone differs

private GregorianCalendar formatDate(Date dateStatus, Time timeStatus) {
    GregorianCalendar calendar = (GregorianCalendar) GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"));
    calendar.setTime(new Date(dateStatus.getTime() + timeStatus.getTime()));
    return calendar;
}

上面的代码returns 以毫秒为单位的日历值。但是我在本地得到了不同的值,而 运行 jenkins 中的测试导致测试用例失败。

本地和 Jenkins 服务器 运行 在不同的时区。

詹金斯错误:

Expected: 1554866100000
     got: 1554903900000

我该如何处理?

  1. java.sql.Date 是一个 非常 不幸的 API 设计混乱。 class 扩展了 java.util.Date,而 class 是 一个谎言 。它 代表一个日期(check the source code 如果你可以理解地持怀疑态度)。它代表一个时刻,没有任何时区信息,基于自 1970 年 UTC 新年以来的毫秒数。任何你想从日期 object 强制转换的不是很大的数字或直接交换到更多因此,不会说谎的适当类型(例如 java.time.Instant 是可疑的 :它正在隐式地选择一个时区并将其混合,以便为您提供答案。这就是为什么 Date 的大多数方法(例如 .getYear())都被标记为弃用的原因:在 java 核心库中,弃用标记通常并不意味着“这将很快被删除”,它实际上意味着:“这从根本上被打破了,你永远不应该调用这个方法”。 (参见:Thread.stop)。

  2. 尽管如此,JDBC API(您用来与 DB 交谈的内容)是在此基础上构建的; java.sql.Date 以及 java.sql.Timestamp 扩展 java.util.Date 从而继承混乱。这意味着以这种方式处理日期 会将数据库中具有完整时区信息的时间戳转换为无时区结构,然后 java-side 您可以添加新的时区 ,这是一种糟糕的方式做事。

  3. 不幸的是 date/time 非常复杂(见下文)并且数据库存储它的方式截然不同;通常是多个略有不同的 date/time 类型,例如 'TIMESTAMP'、'TIME WITH TIME ZONE' 等。

  4. 因此,没有任何独特的建议无论上下文如何都有效:正确的答案取决于您的 JDBC driver 版本、数据库引擎、数据库引擎版本,以及需要。这意味着最好的方法通常是首先了解基础知识,以便您可以适应并找出最适合您的特定情况的方法。

  5. java.util.Calendar更糟。又是一个谎言(它代表时间。不是日历!),API 设计得非常糟糕(非常non-java-like)。第二次尝试 date/time API 导致另一个 date/time 库 (java.time) 是有原因的:这很糟糕。不要使用它。

所以,让我试着解释一下基础知识:

你喜欢早上 8 点起床。现在是中午,您检查 phone。它说 'the next alarm will ring in 20 hours'。现在,您可以在阿姆斯特丹史基浦机场跳上一架协和式飞机,向西飞往纽约。飞行需要3个小时。当你着陆时,你检查你的 phone。它应该说 'the next alarm will ring in 17 hours'(3 小时的飞行时间已经过去),还是应该说:'the next alarm will ring in 23 hours'(你实际上在纽约时间早上 9 点降落,因为它比阿姆斯特丹早 6 小时,所以到当地时间早上 8 点是 23 小时)。正确答案大概是:23 小时。这需要 'local time' 的概念:以年、月、日、小时、分钟和秒表示的时间点 - 但没有时区,甚至没有 'please assume a timezone for me',而是概念 '. .. where-ever 你现在是'.

在你跳上飞机之前,你在阿姆斯特丹的一家理发店预约了return。您于 2022 年 3 月 8 日在 12:00 完成了它。当您查看 phone 时,它显示:“365 天 0 小时 0 分 0 秒”,您已经预约了。如果你飞到纽约,现在应该是“364 天 21 小时 0 分 0 秒”:你目前在纽约的事实与情况没有任何关系。您可能认为 time-as-millis-since-UTC 应该就足够了, 但事实并非如此:想象一下荷兰废除了夏令时(疯狂的想法?No, quite likely actually)。就在木槌敲击桌子并通过法律的那一刻,荷兰将在 2021 年改用夏令时,然后永远呆在那里,就在那一刻?您的 phone 的 'time until barber appointment' 指标应该立即增加 1 小时(或者减少?)。因为那个理发师预约不会重新安排在 11:00 或 13:00.

在飞行途中,您在飞机飞越大西洋之前拍了一张郁金香花田的照片。这张照片的时间戳是另一个时间概念:如果荷兰以某种方式决定追溯应用时区更改,那么 'this picture was taken on 2021, march 8th, 12:05, Amsterdam time' 的时间戳应该 立即弹出现在改为阅读 13:05 或 11:05。这样就不像理发师预约了。

在回答这个问题之前,您需要弄清楚您的情况归结为 3 种不同的时间概念中的哪一种。

只有 java.time 包完全涵盖了这一切。分别为:

  • 闹钟时间由LocalDateLocalTimeLocalDateTime表示。

  • 预约时间用ZonedDateTime表示。

  • 我什么时候做的图片时间用Instant表示。

java.sql.Date 类型最等同于 Instant,这很奇怪,因为你会认为这更像是 java.sql.Timestamp 的范围。

语用学,如何完成工作

您的第一步是充分了解您的数据库引擎的数据类型。例如,在 postgres:

concept java.time type postgres type
alarm clocks java.time.LocalTime time without time zone
- java.time.LocalDate date
- java.time.LocalDateTime timestamp without time zone
appointments java.time.ZonedDateTime timestamp with time zone
when i took the picture java.time.Instant no appropriate type available

java.sql.Datejava.sql.Timestamp 的实现最符合 postgres 无法表达的概念,有点愚蠢,呵呵。

一旦您确保您的数据库为您的概念使用了正确的类型,接下来要检查的是您的 JDBC driver 和其他基础设施是否是最新的。您可以使用正确的类型java-side(来自java.time包):使用这些类型(LocalDateInstantZonedDateTime, 等) 在你的基础设施中,如果进行原始 JDBC 调用,设置时 .setObject(x, instanceOfZDT) 而不是 .setDate,并且 .getObject(col, LocalDateTime.class) 用于获取,这适用于许多 JDBC drivers.

如果它不起作用,您需要解决这些问题,因为该过程现在是数据库正在存储,例如年、月、日、小时、分钟、秒和完整的时区描述(不是 'EST' 有太多的时区无法用 3 个字母覆盖,而是 'Europe/Amsterdam' 之类的),然后将其转换进入 millis-since-epoch 将其传输到 JDBC 服务器,然后您的 java 代码将再次注入时区,因此您将 运行 陷入困境。最好的办法是尽快转换,这样你就有了一个旧的笨重的类型(java.sql.Datejava.sql.Timestamp) 尽可能短,并且可以在源头上测试你是 'undoing the damage'当您的 ZDT/LDT 类型作为 Instant-esque java.sql 类型到达时完成。