Hibernate不能提供UTC时区的日期

5
这似乎是一个愚蠢的问题,但我不理解这种令人毛骨悚然的行为。我完全知道java Date类中没有存储任何时区信息,它只存储自1970年1月1日00:00:00 GMT以来的毫秒数。
问题在于,我正在使用安装在UTC时区服务器上的MySql,并且仅在UTC中存储DateTime。如果我执行select查询,则会得到此日期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在从数据库中提取日期/时间字段时会考虑系统时区。如果你已经在数据库中存储了UTCDateTime,但是你的系统时间或至少你的Java应用程序时区是另一个时区(例如,IST-印度标准时间),那么Hibernate认为在数据库中存储的DateTime也是IST时区的,这就导致了整个问题。

Basil建议的那样,在不同的服务器上使用相同的时区。应该优先选择UTC。我应用的修复方法是添加以下代码:

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

ServletContextListener中,我将我的应用程序时区设置为UTC,现在Hibernate按预期获取和存储日期。


为什么不正确?在UTC中,“17/01/2014 10:45”与IST中的“17/01/2014 16:15”是相同的。在我看来,这是正确的。 - ako
@ako这是不正确的,因为看顶部,mysql输出是16:15:49 GMT,而相同时间的Java输出为04:15 PM IST,或者你可以说它是16:15 IST,而Java中的UTC或GMT是上午10:45。根据MySQL条目,你评论中的16:15不应该是IST,而应该是UTC。 - Abhinav
2
你可以使用选项 -Duser.timezone=UTC 运行JVM。这应该会有所帮助。更多信息请参见链接 - ako
但为什么会出现这种情况?它应该获取正确的毫秒时间。因为“Date”不知道时区。在Hibernate中,有没有方便的方法来实现链接中提到的解决方案? - Abhinav
2
链接,方法setDate(int,java.sql.Date,java.util.Calendar):驱动程序使用Calendar对象构造SQL DATE值,然后将其发送到数据库。 使用Calendar对象,驱动程序可以计算考虑自定义时区的日期。 如果未指定Calendar对象,则驱动程序将使用默认时区,即运行应用程序的虚拟机的时区。 - ako
显示剩余2条评论
2个回答

6

混淆的问题

您的问题需要重新表述。

如果我进行选择查询 - 您应该解释并给出确切的查询语句。

转移注意力

由于处理两个不同的服务器(数据库服务器,Web 应用程序服务器),每个服务器都有不同的时区设置,因此您应该更清晰地分离问题。实际上,MySQL 服务器似乎只是一个转移注意力的话题,一个分散注意力的话题。

与其谈论无关的 MySQL 服务器,您应该测试并报告实际时间。很容易做到...只需在 Google 中搜索“UTC 的当前时间”即可。

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

时区

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

使用时区名称,如在此稍微过时的列表中所见。在您的情况下,+05:30,您可以使用“Asia/Kolkata”,也称为旧表中的“Asia/Calcutta”。那是比UTC / GMT提前5.5小时。

Joda-Time

java.util.Date和Calendar类非常糟糕和令人困惑,正如您所看到的那样。避免使用它们。使用开源第三方Joda-Time或Java 8中的新java.time.*类(受Joda-Time启发)。

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

基线

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

这与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 ≈ 印度标准时间16:15。

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。某些操作系统没有此选择,因此可以选择“Atlantic/Reykjavik”作为解决方法,因为冰岛全年都使用UTC/GMT而没有任何夏令时的麻烦。

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


非常好的建议,我自己也遇到了很多次Java日期和日历类的问题,所有的问题都是基于时区的。JodaTime绝对是一个更好的处理时间的库,但Hibernate没有本地支持它。这就是为什么我只使用Date类。而且,这是一个问题,一旦你把它存储在一个Date对象中,然后你将其转移到Joda中,问题仍然存在。因为日期计算错误的时区日期。 - Abhinav
我已解决了这个问题。我根据您的建议对问题进行了编辑,并描述了解决方案。谢谢! - Abhinav
1
我认为仅仅避免使用java.util.Date或Calendar并不能解决这个问题。这是一个更加复杂的问题,涉及到Java、数据库/驱动程序和Hibernate的综合性问题。 - zd333
@Basil 再次提出了很好的建议。有一个问题,为什么您建议将操作系统时区设置为UTC而不是JVM时区,因为操作系统时区更加严格并包含其中的所有内容。谢谢。 - HopeKing
1
@HopeKing 通常,服务器的最佳实践是将其默认时区设置为UTC。记录日志、错误报告和其他业务应该使用UTC进行处理。系统管理员和程序员应该学会将UTC视为唯一的真实时间,这是一种可靠的时间跟踪,不受诸如夏令时(DST)和无聊政客等异常影响。所有其他时区都只是变体。在桌子上放置一个第二个时钟,设置为UTC。JVM也应该被设置为默认的UTC。由于奇怪的技术问题可能会有例外情况(我曾经遇到过这样的例外情况)。 - Basil Bourque

1

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

在同一页上,您还可以找到详细的安装和应用修复程序的说明。通常,如果您使用的是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注释情况(不使用Spring Boot),只需将此行添加到persistence.xml文件中的<persistence-unit>标记之间:

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

现在您的实体中的日期被视为UTC,当读取/保存到数据库时。如果您想了解Java / Hibernate中日期和时区问题的本质,请阅读此 article 以获取更多详细信息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接