繁体   English   中英

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

[英]Java API and client app - how to handle timezone properly

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

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

现在假设服务器位于伦敦(gmt 0),西班牙用户(gmt +1)和预订从2015年2月25日16:00开始。

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

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

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

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 );

由此我们可以在UTC的时间线上提取Instant ,片刻。

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方法使用PT23H的标准ISO 8601持续时间格式 ,其中P标记开头, T将年 - 月 - 日部分与小时 - 分 - 秒部分分开, H表示“小时”。

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

转储到控制台。

System.out.println ( "zonedBooking: " + zonedBooking + " | booking: " + booking + " | twentyFourHoursEarlier: " + twentyFourHoursEarlier + " | cancellation: " + cancellation + " | cancelledEarlyEnough: " + cancelledEarlyEnough + " | cancellationNotice: " + cancellationNotice );

zonedBooking:2016-02-25T16:00 + 01:00 [欧洲/马德里] | 预订:2016-02-25T15:00:00Z | 二十四小时:2016-02-24T15:00:00Z | 取消:2016-02-24T16:00:00Z | cancelledEarlyEnough:false | cancelNotice: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通过指定一个short-medium-long标志和一个人类语言的Locale进行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数量。

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

使用时间戳验证是一个简单的操作: 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 ,这在处理whith日期时有很大帮助。 将此属性设置为UTCLocalDateTimeZonedDateTime

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM