Hibernate不以UTC时区提供日期

时间:2014-01-17 18:45:57

标签: java hibernate datetime

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

事实是,我使用的是驻留在具有UTC时区的服务器上的MySql,我也只在UTC中存储DateTime。如果我进行选择查询,那么我会得到此日期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会考虑系统时区。如果您已在数据库中的 UTC 中存储了DateTime,但您的系统时间或至少您的Java应用时区位于其他时区(例如,IST - 印度标准时间),那么hibernate认为{{存储在数据库中的1}}也在IST中,这就是导致整个问题的原因。

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

DateTime

TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 中,我的应用时区为 UTC ,现在hibernate正在按预期获取和存储日期。

2 个答案:

答案 0 :(得分:4)

令人困惑的问题

您的问题可能会使用一些重写。

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&正如你所看到的那样,日历类是非常糟糕和令人困惑的。避免他们。使用开源第三方Joda-Time或Java 8中的新java.time.* classes(受Joda-Time启发)。

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

基线

让我们确定1389975349000L≈16:15UTC≈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:45UTC≈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

结论

我不是百分百肯定,但是......

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

网络应用服务器显然是在印度时区的16:15时间,但显然在那一刻印度的真实时间是21:45。换句话说,时间与时区不匹配。

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

如果您设置印度时区,请设置印度时间以匹配。

详细

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

java.util.Date类的设计非常糟糕,它本身没有时区信息 BUT 在其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代码中的那些。

答案 1 :(得分: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 Annotations和HBM文件设置之间有所不同,因此我将仅提供一个可能的设置示例;对于JPA Annotations案例(没有Spring Boot),只需将此行添加到persistence.xml标记之间的<persistence-unit>文件中:

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

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