【问题标题】:Hibernate not providing date in UTC timezoneHibernate 不提供 UTC 时区的日期
【发布时间】:2014-02-07 05:35:39
【问题描述】:

这似乎是一个愚蠢的问题,但我无法理解这种令人毛骨悚然的行为。我完全知道 java Date 类不会在其中存储任何 TimeZone 信息这一事实。它只存储自 1970 年 1 月 1 日 00:00:00 GMT 以来的毫秒数

问题是,我正在使用驻留在具有 UTC 时区的服务器上的 MySql,并且我也仅将 DateTime 存储在 UTC 中。如果我进行选择查询,那么我会得到这个日期2014-01-17 16:15:49

通过使用http://www.epochconverter.com/ 我明白了:

Epoch timestamp: 1389975349
Timestamp in milliseconds: 1389975349000
Human time (GMT): Fri, 17 Jan 2014 16:15:49 GMT
Human time (your time zone): Friday, January 17, 2014 9:45:49 PM

现在是 Hibernate 的一部分。我在以 IST 作为系统时区的机器上运行我的 Java Web 应用程序。我使用 Id 做了一个简单的对象提取,并提取了 createdDate 属性,这是一个 Date 对象。我写了一个简单的代码来理解它的输出,这里是代码:

Date dt = c.getCreatedDate();
System.out.println(dt.getTime());
System.out.println(dt);
DateFormat df = new SimpleDateFormat("dd/MM/yyyy  hh:mm a z");
df.setTimeZone(TimeZone.getTimeZone("IST"));
System.out.println(df.format(dt));
df.setTimeZone(TimeZone.getTimeZone("UTC"));
System.out.println(df.format(dt));

下面是这个的输出:

1389955549000
2014-01-17 16:15:49.0
17/01/2014  04:15 PM IST
17/01/2014  10:45 AM UTC

如果你把这个1389955549000 放在http://www.epochconverter.com/ 中,那么你会得到以下输出:

GMT: Fri, 17 Jan 2014 10:45:49 GMT
Your time zone: Friday, January 17, 2014 4:15:49 PM GMT+5.5

这不是预期的输出,对吧。它给了我以毫秒为单位的时间,即从 UTC 时间开始的 -5:30,所以如果我尝试在 IST 时区获取时间,那么它实际上给了我以 UTC 为单位的时间

有人知道我哪里做错了吗?

--------------我是如何解决的---------- ------------------

基于 - AkoBasil Bourque 的建议

Hibernate 在从数据库中获取日期/时间字段时会考虑系统时区。如果您已将DateTime 存储在数据库中的 UTC 中,但您的系统时间或至少您的 java 应用程序时区位于其他时区(例如,IST - 印度标准时间),则休眠认为 DateTime 已存储在数据库中也在 IST 中,这就是导致整个问题的原因。

为此,正如 Basil 建议的那样,在不同的服务器上使用相同的时区。应该首选UTC。我应用的修复是我添加了以下代码:

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

ServletContextListener 中,这使我的应用程序时区位于UTC,现在休眠正在按预期获取和存储日期。

【问题讨论】:

  • 为什么不正确? UTC 中的“17/01/2014 10:45”与 IST 中的“17/01/2014 16:15”相同。对我来说看起来是正确的。
  • @ako 这是不正确的,因为看看顶部,mysql 输出是格林威治标准时间 16:15:49,而 java 输出同样是 IST 下午 04:15,或者你可以说它是 16:15 IST 而UTC 或根据java,格林威治标准时间是上午10:45。并且您评论中的 16:15 不应该是 IST,根据 mysql 中的条目应该是 UTC
  • 您可以使用选项 -Duser.timezone=UTC 运行 JVM。它应该有帮助。请参阅link 了解更多信息。
  • 但是为什么会这样呢?它应该以毫秒为单位获取正确的时间。因为Date 不知道时区。您在hibernate中给出的链接中提到的解决方案有什么方便的方法吗?
  • 来自link,方法setDate(int, java.sql.Date, java.util.Calendar):驱动程序使用Calendar 对象构造一个SQL DATE 值,然后驱动程序将其发送到数据库。使用 Calendar 对象,驱动程序可以在考虑自定义时区的情况下计算日期。如果未指定日历对象,则驱动程序使用默认时区,即运行应用程序的虚拟机的时区。

标签: java hibernate datetime


【解决方案1】:

还有另一种解决时区偏移问题的方法;在这种情况下,您不必将整个 JVM 放在 UTC 时区中(无论如何这都不是一个好主意;例如,您可能希望在多个 Java 应用程序之间共享 JVM,而这种 hacky 解决方案可能会导致未来)。

所以,有一个小的开源项目DbAssist,它为这个问题提供了一个干净优雅的解决方案。在内部,它将实体中的java.util.Date 字段映射到自定义UtcDateType。自定义类型强制 JDBC(以及后来的 Hibernate)将数据库中的日期视为 UTC。对于不同版本的 Hibernate,此修复程序有不同的版本(其 API 在版本之间更改了几次),因此您必须根据表 here 选择正确的版本。

在同一页面上,您还可以找到如何安装和应用修复程序的详细说明。通常,如果您使用的是 Hibernate 5.2.2,只需将以下行添加到您的 POM 文件中:

<dependency>
    <groupId>com.montrosesoftware</groupId>
    <artifactId>DbAssist-5.2.2</artifactId>
    <version>1.0-RELEASE</version>
</dependency>

然后,JPA 注释和 HBM 文件设置之间的修复应用程序不同,因此我将仅提供一种可能设置的示例;对于 JPA Annotations 案例(没有 Spring Boot),只需将此行添加到 persistence.xml 文件之间的 &lt;persistence-unit&gt; 标签:

<class>com.montrosesoftware.dbassist.types</class>

现在,实体中的日期在读取/保存到数据库时被视为 UTC。如果你想了解更多关于 Java/Hibernate 中日期和时区问题的本质,你可以阅读这个article 详细解释它。

【讨论】:

    【解决方案2】:

    令人困惑的问题

    您的问题可能需要重写。

    If I make a select query – 你应该解释一下并给出准确的查询语句。

    红鲱鱼

    由于要处理两个独立的服务器(数据库服务器、Web 应用程序服务器),每个服务器都有不同的时区设置,因此您应该更清楚地分开问题。事实上,MySQL 服务器似乎只是一条红鲱鱼,一种分散注意力的东西。

    您应该测试并报告实际时间,而不是谈论无关紧要的 MySQL 服务器。很容易做到……只需谷歌“UTC当前时间”。

    因此,老水手的格言是:使用一个或三个指南针,但永远不要使用两个。

    时区

    三字母时区代码已过时,既不标准化也不唯一。例如,“IST”表示“印度标准时间”“爱尔兰标准时间”。

    使用时区名称,如slightly outdated list 所示。在您的情况下,+05:30,您可以在旧表中使用“Asia/Kolkata”,也称为“Asia/Calcutta”。这比UTC/GMT 提前了 5.5 小时。

    乔达时间

    如您所见,java.util.Date 和 Calendar 类是出了名的糟糕和混乱。避开他们。使用开源的第三方 Joda-Time,或者在 Java 8 中使用新的 java.time.* classes(受 Joda-Time 启发)。

    以下代码在具有美国西海岸时区的 Mac 上使用 Joda-Time 2.3 和 Java 8。

    基线

    让我们确定 1389975349000L ≈ 16:15 UTC ≈ 21:45 India。

    正如问题所述,这与 EpochConverter.com 一致。

    // Specify a time zone rather than depend on defaults.
    DateTimeZone timeZoneKolkata = DateTimeZone.forID( "Asia/Kolkata" );
    
    long millis = 1389975349000L;
    DateTime dateTimeUtc = new DateTime( millis, DateTimeZone.UTC );
    DateTime dateTimeKolkata = dateTimeUtc.toDateTime( timeZoneKolkata );
    

    转储到控制台...

    System.out.println( "millis: " + millis );
    System.out.println( "dateTimeUtc: " + dateTimeUtc );
    System.out.println( "dateTimeKolkata: " + dateTimeKolkata );
    

    运行时……

    millis: 1389975349000
    dateTimeUtc: 2014-01-17T16:15:49.000Z
    dateTimeKolkata: 2014-01-17T21:45:49.000+05:30
    

    神秘号码

    问题提到了第二个数字:1389955549000L。

    这个数字与第一个数字的日期相同,但时间不同。

    让我们确定 1389955549000L ≈ 10:45 UTC ≈ 16:15 India。

    long mysteryMillis = 1389955549000L;
    DateTime mysteryUtc = new DateTime( mysteryMillis, DateTimeZone.UTC );
    DateTime mysteryKolkata = mysteryUtc.toDateTime( timeZoneKolkata );
    

    转储到控制台...

    System.out.println( "mysteryMillis: " + mysteryMillis );
    System.out.println( "mysteryUtc: " + mysteryUtc );
    System.out.println( "mysteryKolkata: " + mysteryKolkata );
    

    运行时……

    mysteryMillis: 1389955549000
    mysteryUtc: 2014-01-17T10:45:49.000Z
    mysteryKolkata: 2014-01-17T16:15:49.000+05:30
    

    结论

    我不是 100% 确定,但是……

    → 您的 Web 应用服务器计算机的时钟设置似乎不正确,设置为 UTC 时间而不是印度时间。

    Web 应用服务器显然是让印度时区的时间为 16:15,但显然当时印度的真实时间是 21:45。换句话说,时间与时区不匹配。

    将 UTC 时间与非 UTC 时区混合 = 错误。

    如果您设置了印度时区,则设置一个印度时间来匹配。

    详情

    请注意,我们在两组数字中都有“16:15”。

    java.util.Date 类的设计非常糟糕,它本身没有时区信息但是在其toString 的实现中应用了Java 虚拟机的默认时区。这是避免此类的众多原因之一,但可能是您问题的关键。

    经验教训

    避免使用 java.util.Date、java.util.Calendar 和 java.text.SimpleDateFormat 类。仅在需要时使用,以便根据需要与其他类交换值。

    使用 Joda-Time 或新的 java.time.* 类 (JSR 310)。

    指定时区,不要依赖默认时区。

    匹配每台服务器上的时区。通过谷歌搜索“utc 中的当前时间”进行验证。

    尽可能将主机服务器的操作系统时区设置为UTCGMT。在某些操作系统上没有这样的选择,因此选择“大西洋/雷克雅未克”作为解决方法,因为冰岛全年保持 UTC/GMT,没有任何夏令时废话。

    不要不要通过调用TimeZone.setDefault来设置JVM的默认时区(除非在最坏的情况下作为最后的手段)。设置默认值是粗鲁的,因为它会立即影响在该 JVM 中运行的所有应用程序的所有线程中的所有代码。而且它是不可靠的,因为任何其他代码都可以在运行时在您的应用程序上更改它。相反,请在所有日期时间代码中指定时区。永远不要隐式依赖 JVM 的当前默认值。 Joda-Time 和 java.time 都有采用时区参数的方法。因此,虽然将所有服务器计算机的主机操作系统的时区设置为 UTC 是一种很好的做法,但您应该永远在 Java 代码中依赖它。

    【讨论】:

    • 很好的建议,我自己在 java 日期和日历类方面遇到了数十个时间问题,所有问题都是基于时区的。 Jodatime 绝对是一个更好的处理时间的库,但 Hibernate 没有对它的原生支持。这就是为什么我只坚持使用Date 类。这是一个问题,一旦将其存储在Date 对象中,然后将其转移到 Joda 中,问题就会保持不变。因为日期计算了错误的时区日期。
    • 我解决了这个问题。我根据您的建议对描述解决方案的问题进行了编辑。谢谢!
    • 我不认为简单地避免 java.util.Date 或 Calendar 会解决这个问题。并且更多涉及Java、Database/Driver和Hibernate之间的组合问题。
    • @Basil 再次提出很好的建议。一个问题,您为什么建议将操作系统时区设置为 UTC 而不是 JVM 时区,因为操作系统时区更具限制性并且包含其中的所有内容。谢谢。
    • @HopeKing 通常,服务器的最佳实践是将其默认时区设置为 UTC。日志记录、错误报告和其他业务应该在 UTC 中完成。系统管理员和程序员应该学会将 UTC 视为一个真实的时间,一个可靠的时间跟踪,没有诸如夏令时 (DST) 和无聊的政客之类的异常情况。所有其他区域都只是变体。将桌面上的第二个时钟设置为 UTC。 JVM 还应设置为默认为 UTC。由于奇怪的技术问题,可能会有例外(我有一个这样的例外)。
    猜你喜欢
    • 1970-01-01
    • 2019-09-07
    • 2016-08-07
    • 2013-08-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-10
    相关资源
    最近更新 更多