【问题标题】:Can I force Java to ignore timezones?我可以强制 Java 忽略时区吗?
【发布时间】:2016-05-26 10:58:29
【问题描述】:

短版: 我可以强制 Java 不知道时区吗?

加长版: 我有一个较旧的、基于 Java 6 的三层系统,带有一个 Java GUI 前端(在用户 PC 上运行)、一个 Java/OSGi 中间件服务器和一个 Oracle 11g 数据库后端。两台服务器位于集群内的同一机架中。存储在数据库中的主要实体(检查)有一个日期列,该列存储检查发生瞬间的日期和时间。日期和时间在 GUI 中作为字符串输入,并存储在 DB 中,如下所示:

(theInspection.getInspectionDate() returns a java.util.Date object)

inspectionDate = new java.sql.Timestamp(theInspection.getInspectionDate().getTime());
...
statementHandler.openPreparedStatement(C_INSERT_INTO_INSPECTIONS);
...
statementHandler.setTimestamp(4, inspectionDate);

然后将检查日期和时间读回中间件服务器,以识别和定位文件名中包含日期和时间的原始数据文件。从 DB 读取如下:

resultSet.getDate("inspection_date");
if (resultSet.wasNull()) {
    inspection.setInspectionDate(null);
    } else {
        date = new java.util.Date(resultSet.getDate("inspection_date").getTime() 
                    + resultSet.getTime("inspection_date").getTime() + (3600 * 1000L) );
        inspection.setInspectionDate(date);
    }

现在,这在 CET/CEST (UTC+1) 时区运行多年,但将其移至 MSK (UTC+3) 时会破坏它。时间在存储到 DB 时移动了 +2 小时,并在读回时再次移动了 +2 小时。我已经确定是 Java 执行此操作,Oracle 在此设置中是时区无知的。

我正在考虑更改此设置,以便中间件服务器在存储时转换为 UTC,并在读取时转换回本地时区。我知道 java.util.Date 已被弃用,并且还有其他表示时间和日期的方法。但是,我担心如果 GUI 客户端和服务器位于不同的时区,或者服务器或数据跨时区移动会发生什么。

最后,强制 Java 不知道时区可能是最有意义的。这可能吗?

谢谢!

【问题讨论】:

  • inspection_date 列的数据类型是什么?日期、时间戳、带时区的时间戳、带本地时区的时间戳?
  • 日期,没有任何时区数据。
  • 两台服务器的操作系统时区是否相同; Java 是否依赖于它,或者您是否指定了覆盖操作系统的语言环境?
  • 是的,两台服务器都在同一个时区。没有覆盖操作系统的语言环境。 GUI 客户端可能位于不同的时区,但 java 到数据库的转换发生在服务器上,我相信这是插入偏移量的地方。
  • 您能否告诉我们您使用的是哪个版本的 Oracle JDBC 驱动程序?他们改变了从 9.2 版开始的日期行为,请参见:oracle.com/technetwork/database/enterprise-edition/…

标签: java oracle date datetime timezone


【解决方案1】:

Date 使用隐含的时区这一事实让您感到震惊。
Date 定义为自“1970-01-01T00:00:00”UTC 以来的毫秒数。因此,如果将日期(+时间)字符串转换为应用不同时区的 Date 实例,则毫秒数(对于内部值)会有所不同。

不使用 String-to-Date 转换指定明确的时区并且转换为 DB 值并不意味着这些值不受时区的影响。

一旦这个隐含的时区在代码不知情的情况下“悄悄地”改变,这种改变就可以作为时间“扭曲”观察到。

这种“无声”的时区更改将例如发生在以下场合(不完整的清单!):

  • 通过“线路”发送Date 对象实例,从一个时区到另一个时区
  • 解析时区的日期不是与值相关的隐含时区
  • (错误)代码的隐式时区假设 日期/时间值并尝试对其应用错误的“更正”。 (例如,当一个无 timzone 的 DB 值被归因于来自 DB 的隐含时区时,读取应用程序代码会尝试将其调整为应用程序假定的(不同的)本地时区。)

为了克服这种麻烦,基本上有两种策略:

  1. 在整个应用程序中严格执行唯一的时区。 (考虑到 Date 的隐含语义,使用 UTC 将是一个不错的选择。)
    在这种情况下,您将立即将从“外部”输入的任何值转换为您的公共应用程序时区(从已知或隐含的本地时区)。并且仅将(内部)日期/时间值转换为本地时区以便显示(尽可能晚)。您的应用程序“内部”的任何值(并与数据库一起存储)都将位于应用程序时区。
    这对于更改服务器和数据库的时区是不变的,并且只需要正确的检测日期 I/O 位置。您还需要处理与其他外部实体相关的日期/时间值(例如您提到的文件名)。这些值最好首先根据应用程序时区生成。

  2. 您放弃在没有时区概念的情况下勉强过活,转而使用具有任何日期/时间值的明确时区。

第一种方法的好处是在不同时区提供一致的值(例如,使用日期/时间值生成文件名 - 然后需要将其用作 UTC 值)。

第二个是轻松提供来自不同时区的事件的一致排序。

在存储/读取值时进行时区转换 来自 DB 的客户端可能存在误解的潜在风险,可能存在于完全不同的时区或在存储和检索此类值之间更改时区(正如您已经怀疑的那样)。


并明确解决主题的问题:

不,只要您使用 java Date 类,您将始终在从/到 String 的转换时引用隐式时区,而内部值基于 UTC

【讨论】:

  • 我终于可以访问生产集群并将所有时区更改为UTC+1,这与使用多年的集群相同。我原以为“+1”应该给出与“+3”相同的问题,但显然它没有!现在一切都很顺利。问题解决了! :-)
【解决方案2】:

JDBC 提供了在读取或写入日期期间设置时区的方法。

以下是时间戳的方法。

ResultSet getTimeStamp
PreparedStatement setTimeStamp

将这些方法与如下构造的 Calendar 实例一起使用。

Calendar gmtCalendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));

在编写代码时会变成

statementHandler.setTimestamp(4, inspectionDate, gmtCalendar);

在阅读时会是

date = new java.util.Date(resultSet.getDate("inspection_date", gmtCalendar).getTime() 
                    + resultSet.getTime("inspection_date", gmtCalendar).getTime());

当您使用getTime()(毫秒数)时,您阅读的时间是GMT/UTC,这就是解决问题所必需的。因为时间戳(毫秒数)默认为GMT/UTC

剩下的就是指示JDBC 考虑那个时区。

有关主题https://stackoverflow.com/a/37103310/5343269的更多详细信息,请参阅此 SO 条目

【讨论】:

  • 谢谢,我已经试过了,但是没有用。 Java 中一定有一些东西在 JDK 6 中难以控制。
  • 我在一个项目中使用了与 JDK 6 和 Oracle 相同的配置,这应该可以工作。
猜你喜欢
  • 1970-01-01
  • 2014-02-06
  • 2021-10-26
  • 2011-01-09
  • 1970-01-01
  • 2016-08-23
  • 1970-01-01
  • 1970-01-01
  • 2011-02-23
相关资源
最近更新 更多