三个偏移量
您没有向我们展示您的所有代码,因此我们无法肯定地回答。但是经过一些推论,我有一个理论,一个三偏移理论。
从比 UTC 晚 8 小时的值开始 → 2016-12-12 10:06:39.582-08
调整为 UTC → 2016-12-12 18:06:39.582Z
调整为+05:30的偏移量→2016-12-12 23:36:39.582+05:30
尴尬地报告了无偏移量→2016-12-12 23:36:39.582
感谢the Answer of Laurenz Albe解决核心问题;我只是在这里添加了一个冗长的叙述。
几个术语:
-
offset-from-UTC 是从 UTC 开始的小时数、分钟数和秒数,例如
+05:00。
- 时区是一个偏移量加上一组用于处理异常情况的规则,例如夏令时 (DST)。正确命名为
continent/region,例如America/Montreal。
您的输入 2016-12-12 10:06:39.582-08 表示比 UTC 晚 8 小时,在北美西海岸的大部分地区使用。
您将该值提交给 Postgres 进行存储。您没有准确地说,但我假设表中的列是TIMESTAMP WITH TIME ZONE 类型。
在 Postgres 中,这种类型的意思是:“对于任何附带的偏移量/区域信息,将传入的值调整为 UTC”。 Postgres 然后丢弃随附的偏移量/区域信息。我再说一遍,Postgres 不 存储或记住偏移量/区域,而不是任何日期时间类型。 WITH 类型和 TIMESTAMP WITHOUT TIME ZONE 之间的区别在于,在 WITHOUT 类型中,任何随附的偏移量/区域信息都将被完全忽略,不进行任何调整。
因此,当您向 Postgres 提交 2016-12-12 10:06:39.582-08 时,该值会自动调整为 2016-12-12 18:06:39.582Z。 Z 这里是 Zulu 的缩写,意思是 UTC。此外,Postgres 不按字面意思存储这些字符串,而是使用其自己的内部二进制存储格式。这些字符串供我们人类在本次讨论中阅读。
无论如何,回到对 UTC 的调整。请注意时间是如何从 10 小时更改为 18 小时的,因为从 UTC 晚 8 小时调整为 UTC 意味着增加了 8 小时。
所以,很好,存储这个值的工作已经完成。我们记录了完全相同的时刻,2016-12-12 10:06:39.582-08 和 2016-12-12 18:06:39.582Z 都是相同的时刻,但通过两个不同的 wall-clock time 值的镜头可以看到。到目前为止,一切都是正确和明智的。
后来,您从 Postgres 数据库中检索了 2016-12-12 18:06:39.582Z 值。显然,您通过 JDBC 将数据提取到 java.sql.Timestamp 对象中。问题就在于此。此类是与 Java 的最早版本捆绑在一起的旧日期时间类之一。虽然作为行业领先的日期时间处理尝试值得称赞,但这些课程已被证明是令人困惑、麻烦和有缺陷的。尽可能避免使用它们。它们现在是 legacy,被 java.time 类所取代。
在这些遗留日期时间类的许多问题中,它们的toString 方法的实现。你的toString 方法过于急于取悦隐式应用 JVM 的当前时区。这些对许多 Java 程序员造成了无穷无尽的困惑。添加这个问题作为这种混淆的另一个例子。
首先,让我们演示一下时区的这种隐式应用。首先以Instant 的形式获取UTC 的当前时刻。
Instant instant = Instant.now ();
查看我们的 JVM 当前的默认时区及其偏移量。
ZoneId z = ZoneId.systemDefault (); // Get current default time zone of this JVM.
String offset = z.getRules ().getOffset ( instant ).toString (); // Extract the offset-from-UTC from our default time zone.
最后,创建一个java.sql.Timestamp 以查看其toString 方法的行为。
java.sql.Timestamp ts = new java.sql.Timestamp ( instant.toEpochMilli () );
转储到控制台。
System.out.println ( "instant.toString(): " + instant );
System.out.println ( "z.toString(): " + z );
System.out.println ( "offset.toString(): " + offset );
System.out.println ( "ts.toString(): " + ts );
instant.toString(): 2016-12-13T23:06:22.635Z
z.toString(): America/Los_Angeles
offset.toString(): -08:00
ts.toString(): 2016-12-13 15:06:22.635
从此输出中我们可以看到Timestamp 确实将我自己的当前默认值-08:00 偏移量应用于UTC 的内部值。更糟糕的是,这个方法的输出忽略了它的偏移量的任何指示,让我们假设它是 UTC,而实际上不是。更令人沮丧的是:this ‘feature’ is entirely undocumented!
现在我们已经看到了这个反特征的行为,我们可以回到问题中的示例数据。显然,结果数据是2016-12-12 23:36:39.582,没有我们上面提到的任何偏移量的迹象。但是看看一天中的时间,23:36:39.582 与我们可能预期的 UTC,18:06:39.582。意外的值比我们预期的 UTC 值早五个半小时,即+05:30。 +05:30 的偏移量恰好是 used in India 和斯里兰卡,在诸如 Asia/Kolkata 之类的区域中。所以我们可以推断出这个问题的作者正在JVM上运行她的代码,当前默认时区类似于Asia/Colombo或Asia/Kolkata,偏移量为+05:30。
解决方案
避免使用旧的日期时间类。
改用 java.time 类。 java.time 类都有简单的toString 方法,可以合理地使用标准的ISO 8601 文本格式,没有任何意外的奖励行为。
您的JDBC 4.2 兼容driver 可以通过调用PreparedStatement::setObject 和ResultSet::getObject 直接寻址java.time 类型。
myPreparedStatement.setObject( … , instant ) ;
……和……
Instant instant = myResultSet.getObject( … ) ;
如果不是,则回退到使用 java.sql 类型,但要尽可能简短。使用添加到旧类的新转换方法。
myPreparedStatement.setTimestamp( … , java.sql.Timestamp.from( instant ) ) ;
……和……
Instant instant = myResultSet.getTimestamp( … ).toInstant() ;
关于java.time
java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.Date、Calendar 和 SimpleDateFormat。
Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。
要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310。
从哪里获取 java.time 类?
ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如Interval、YearWeek、YearQuarter 和more。