Java API 和客户端应用程序 - 如何正确处理时区



我正在开发一个被Android应用程序使用的Java API。我已经到了需要正确处理日期和时间并考虑时区的地步。

该应用程序的功能之一是预订指定日期和时间的某种服务。用户可以取消预订,如果在预订开始前24小时取消,客户将获得所有退款。

现在假设服务器在伦敦(GMT 0),用户在西班牙(GMT +1),预订开始于25-02-2015 16:00:00。

当用户取消预订时,服务器需要在 NOW() 和预订开始日期之间做出区分。因此,如果用户(在西班牙)取消24-02-2015在17:00:00(西班牙时间,预订前23小时;因此他不会获得全额退款)当服务器检查差额时,因为现在(英国是16:00:00)结果将是24h,因此会错误地全额退款。

我的问题在这里。如何根据用户时区正确获得正确的小时数?我对在取消请求中发送客户端时区不是很满意,因为该值很容易被用户欺骗。

存储日期和时间服务器端时有什么好的做法?我是否应该将它们存储在服务器时间并使用额外的字段来了解客户端时区偏移量?

Baranauskas的答案是正确的。

在 UTC 中工作

通常,所有业务逻辑、数据交换、数据存储和序列化都应使用 UTC 完成。仅在需要/预期的地方应用时区,例如向用户演示。

java.time

一定要使用Java 8及更高版本内置的java.time框架。

默认情况下,java.time 类使用标准 ISO 8601 格式来解析/生成日期时间值的文本表示形式。否则,请使用DateTimeFormatter对象。

Local…类型是通用的,不适用于任何位置,没有时区或 UTC 偏移量。通常不用于业务应用程序,因为它们不是时间线上的实际时刻。此处用于分别分析日期字符串和时间字符串,合并,然后应用西班牙的已知时区。这样做会产生一个ZonedDateTime,一个时间轴上的实际时刻。

LocalDate dateBooking = LocalDate.parse ( "2016-02-25" ); // Parses strings in standard ISO 8601 format.
LocalTime timeBooking = LocalTime.parse ( "16:00:00" );
LocalDateTime localBooking = LocalDateTime.of ( dateBooking , timeBooking );
ZoneId zoneId = ZoneId.of ( "Europe/Madrid" );
ZonedDateTime zonedBooking = localBooking.atZone ( zoneId );

从中我们可以提取一个 Instant ,UTC时间轴上的一个时刻。

Instant booking = zonedBooking.toInstant ();

计算我们要求的 24 小时取消通知。请注意,24 小时与"一天"不同,因为由于夏令时 (DST) 等异常情况,天数的长度会有所不同。

Instant twentyFourHoursEarlier = booking.minus ( 24 , ChronoUnit.HOURS );

获取正在取消的用户的当前时刻。在这里,我们使用问题中指定的日期时间进行模拟。对于较短的代码,我们调整一小时,因为这个解析方法只处理UTC(Z)字符串。问题在西班牙时间17:00; Europe/Madrid比 UTC 早一小时,所以我们为 16:00Z 减去一小时。

Instant cancellation = Instant.parse ( "2016-02-24T16:00:00Z" );  // 17:00 in Europe/Madrid is 16:00Z.
//  Instant cancellation = Instant.now ();  // Use this in real code.

通过调用isBeforeisAfter方法比较Instant对象。

Boolean cancelledEarlyEnough = cancellation.isBefore ( twentyFourHoursEarlier );

Duration将时间跨度表示为总秒数加上几分之一秒(以纳秒为单位)。我们在这里使用它来帮助数学,验证我们得到了 23 小时的预期结果。toString方法使用标准的ISO 8601持续时间格式PT23H其中P标记开始,T年-月-日部分与小时-分钟-秒部分分开,H表示"小时"。

Duration cancellationNotice = Duration.between ( cancellation , booking );

转储到控制台。

System.out.println ( "zonedBooking: " + zonedBooking + " | booking: " + booking + " | twentyFourHoursEarlier: " + twentyFourHoursEarlier + " | cancellation: " + cancellation + " | cancelledEarlyEnough: " + cancelledEarlyEnough + " | cancellationNotice: " + cancellationNotice );
分区预订: 2016-02-25T16:00+01:00[欧洲/马德里] | 预订: 2016-02-25T15:00:00Z | 二十四小时提前: 2016-02-24T15:00:00Z | 取消:

2016-02-24T16:00:00Z | 取消足够早: 假 | 取消通知: PT23H

通过应用用户所需/预期的时区向用户演示。指定InstantZoneId以获取ZonedDateTime

ZoneId zoneId_Presentation = ZoneId.of( "Europe/Madrid" );
ZonedDateTime zdtBooking = ZonedDateTime.ofInstant( booking , zoneId_Presentation );
ZonedDateTime zdtCancellationGrace = ZonedDateTime.ofInstant( twentyFourHoursEarlier , zoneId_Presentation );
ZonedDateTime zdtCancellation = ZonedDateTime.ofInstant( cancellation , zoneId_Presentation );

让java.time为您本地化,为人类语言指定一个短-中-长标志和一个Locale,以(a)翻译日/月的名称,(b)使用文化规范来排序日期时间部分,选择逗号与句点,等等。

DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.FULL );
formatter = formatter.withLocale( new Locale("es", "ES") );
String output = zdtBooking.format( formatter );

Jueves 25 de Febrero de 2016 16H00' CET

忽略服务器时区

您的问题提到了服务器的时区。您永远不应该依赖服务器的时区,因为它是您无法作为程序员控制的,并且系统管理员几乎不需要考虑就可以轻松更改它。此外,JVM 的当前缺省时区可能基于主机操作系统的时区。但不一定。在命令行启动 JVM 时,标志可以设置 JVM 的缺省时区。更危险的是:JVM中任何应用程序的任何线程上的任何代码都可以设置时区并立即影响您的应用程序 - 在运行时!

如果未指定时区,那么将隐式应用 JVM 的当前缺省值。始终显式指定所需/预期的时区,如上面的代码所示。

以上也适用于当前默认Locale。始终指定所需/预期的Locale,而不是隐式依赖于 JVM 的当前缺省值。

无论您身在何处

,飞机都会在同一时刻离开。这也称为时间戳。对于这种情况,存储时间戳将是一个合适的解决方案。在 java 中,可以通过 System.currentTimeMillis() 检索当前时间戳。此值不依赖于服务器的时区,并且包含自 1970 年以来以 UTC 表示的 millis 数量。

当用户预订航班时,您需要将用户选择的时间+时区转换为时间戳。显示时,时间戳应转换为用户的时区。

使用时间戳进行验证是一个简单的操作:planeTakeOffTs - NOW > 24*60*60*1000

您可能还想使用joda-time这样的库(已包含在受启发的Java v8 java.time中)来处理日期和时间。

Basil Bourque,为了回答您的最后一条评论,我在下面进行了测试。如您所见,设置时区对LocalDateTime和幸运有影响。你对的地方是这不是正确的解决方案,如果我们没有其他解决方案,应该使用。

public static void main(String[] args) {
    Instant instant = Instant.now();
    LocalDateTime local = LocalDateTime.now();
    ZonedDateTime zone = ZonedDateTime.now();
    System.out.println("====== WITHOUT 'UTC' TIME ZONE ======");
    System.out.println("instant           : " + instant );
    System.out.println("local             : " + local);
    System.out.println("zone              : " + zone);
    System.out.println("instant converted : " + instant.atZone(ZoneId.of("Europe/Paris")).toLocalDateTime());
    System.out.println("====================================");
    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    Instant instant2 = Instant.now();
    LocalDateTime local2 = LocalDateTime.now();
    ZonedDateTime zone2 = ZonedDateTime.now();
    System.out.println("====== WITH 'UTC' TIME ZONE ======");
    System.out.println("instant2           : " + instant2 );
    System.out.println("local2             : " + local2);
    System.out.println("zone2              : " + zone2);
    System.out.println("instant2 converted : " + instant2.atZone(ZoneId.of("Europe/Paris")).toLocalDateTime());
    System.out.println("==================================");
}

和输出:

====== WITHOUT 'UTC' TIME ZONE ======
instant           : 2019-02-14T17:14:15.598Z
local             : 2019-02-14T18:14:15.682
zone              : 2019-02-14T18:14:15.692+01:00[Europe/Paris]
instant converted : 2019-02-14T18:14:15.598
====================================
====== WITH 'UTC' TIME ZONE ======
instant2           : 2019-02-14T17:14:15.702Z
local2             : 2019-02-14T17:14:15.702
zone2              : 2019-02-14T17:14:15.702Z[UTC]
instant2 converted : 2019-02-14T18:14:15.702
==================================

请注意,由于许多开发人员使用 Hibernate,它提供了此属性hibernate.jdbc.time_zone这在处理日期时有很大帮助。将此属性设置为 UTCLocalDateTimeZonedDateTime 生效。

相关内容

  • 没有找到相关文章

最新更新