阅读问题Daylight saving time and time zone best practices。你的基本上是重复的。
UTC 服务器
是的,通常服务器应将其操作系统设置为UTC 作为时区,或者如果未提供,则使用GMT 或Reykjavík Iceland time zone。您的 Java 实现可能会将此设置作为其自己的当前默认时区。
指定时区
但不要依赖于设置为 UTC 的时区。系统管理员可以更改它。 JVM 内任何应用程序的任何线程中的任何 Java 代码都可以通过调用 TimeZone.setDefault 来更改 JVM 当前的默认时区在运行时。因此,请养成始终通过在 Java 代码中传递可选参数来指定所需/预期时区的习惯。
我认为任何日期时间框架都会使时区成为可选的设计缺陷。可选会造成无穷无尽的混乱,因为程序员和其他人一样,除非有提示,否则会无意识地根据自己的个人时区进行思考。因此,在日期时间的工作中,常常没有注意到这个问题。加上 JVM 默认值不同的问题。顺便说一句,Locale 同上,同样的问题,应该始终明确指定。
UTC
您的业务逻辑、数据存储和数据交换几乎都应该在UTC 中完成。几乎每个数据库都具有将任何输入调整为 UTC 并以 UTC 存储的功能。
向用户显示日期时间时,请调整到预期的时区。序列化日期时间值时,使用ISO 8601 字符串格式。请参阅 VickyArora 为 Oracle 撰写的 the Answer(我是 Postgres 人)。请务必仔细阅读文档,并通过实验来充分理解数据库的行为。 SQL 规范在这方面没有详细说明,而且行为差异很大。
java.sql
请记住,当使用 Java 和 JDBC 时,您将使用 java.sql.Timestamp 和相关的数据类型。它们总是自动使用 UTC。将来期望看到更新 JDBC 驱动程序以直接使用 Java 8 及更高版本中内置的 java.time 框架中定义的新数据类型。
java.time
旧类已被java.time 淘汰。 学习使用 java.time,同时避免使用旧的 java.util.Date/.Calendar,让您的编程生活更加愉快。
在您的JDBC driver 更新之前,您可以使用java.time 中内置的转换便利方法。请看下面的示例,其中Instant 是 UTC 中的时刻,ZonedDateTime 是调整为 time zone 的 Instant。
Instant instant = myJavaSqlTimestamp.toInstant();
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );
去另一个方向。
java.sql.Timestamp myJavaSqlTimestamp = java.sql.Timestamp.from( zdt.toInstant() );
如果您需要原始时区,请存储它
如果您的业务需求认为原始输入数据的时区很重要,需要记住,则将其显式存储为数据库表中的单独列。您可以使用offset-from-UTC,但这并不能提供完整的信息。时区是一个偏移量加上一组过去、现在和将来异常处理的规则,例如Daylight Saving Time。所以proper time zone name 是最合适的,例如America/Montreal。
仅日期不明确
您说您收集了许多仅日期值,没有时间和时区。 java.time 中的类是LocalDate。与LocalTime 和LocalDateTime 一样,“本地...”部分表示没有特定的地点,因此没有时区,因此也不是时间线上的一个点——没有真正的意义。
请记住,仅日期值在定义上是不明确的。在任何特定时刻,世界各地的日期都会有所不同。例如,Paris France 的午夜刚过是新的一天,但Montréal Québec 的日期仍然是“昨天”。
通常在商业中,某些时区是隐含的,甚至是无意识的直觉。从长远来看,对数据点的无意识直觉往往效果不佳,尤其是在软件中。最好明确说明预期的时区。您可以将预期区域存储在日期旁边,例如数据库表中的另一列,或者您可以在您的编程代码中进行注释。我相信存储日期时间值会更好、更安全。那么我们如何将仅日期转换为日期时间呢?
新的一天通常是午夜过后的那一刻,即一天的第一刻。您可能认为这意味着一天中的时间00:00:00.0,但并非总是如此。 Daylight Saving Time (DST) 和可能的其他异常可能会将第一时刻推到不同的 wall-clock time。让 java.time 确定第一次通过 LocalDate 类及其 atStartOfDay 方法的正确时间。
ZoneId zoneId = ZoneId.of( "America/Montreal" );
LocalDate today = LocalDate.now( zoneId );
ZonedDateTime todayStart = today.atStartOfDay( zoneId );
在某些业务环境中,新的一天可能被定义(或假定)为工作时间。例如,假设New York 的出版商在他们说“书稿将于 1 月 2 日到期”时表示他们当地时间上午 9 点。让我们在那个时区获取那个日期的那个时间。
ZoneId zoneId = ZoneId.of( "America/New_York" );
ZonedDateTime zdt = ZonedDateTime.of( 2016 , 1 , 2 , 9 , 0 , 0 , 0 , zoneId );
这对在New Zealand 工作的作者意味着什么?通过调用withZoneSameInstant调整到她的particular time zone以向她展示。
ZoneId zoneId_Pacific_Auckland = ZoneId.of( "Pacific/Auckland" );
ZonedDateTime zdt_Pacific_Auckland = zdt.withZoneSameInstant( zoneId_Pacific_Auckland );
数据库
对于数据库存储,我们转换为Instant(UTC 时间线上的时刻)并作为java.sql.Timestamp 传递,如上文所述。
java.sql.Timestamp ts = java.sql.Timestamp.from( zdt.toInstant() );
从数据库中检索时,转换回纽约日期时间。从java.sql.Timestamp 转换为Instant,然后应用时区ZoneId 以获得ZonedDateTime。
Instant instant = ts.toInstant();
ZoneId zoneId = ZoneId.of( "America/New_York" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );
如果您的database driver 符合JDBC 4.2 或更高版本,您可以直接传递/获取java.time 类型,而不是转换为java.sql 类型/从java.sql 类型转换。试试PreparedStatement::setObject 和ResultSet::getObject 方法。