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

Joh*_*y19 2 java api timezone client

我正在开发一个由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)结果将是24小时,所以将错误地退还全额.

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

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

Bas*_*que 5

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 );
Run Code Online (Sandbox Code Playgroud)

由此我们可以Instant在UTC的时间线上提取一个时刻.

Instant booking = zonedBooking.toInstant ();
Run Code Online (Sandbox Code Playgroud)

计算我们24小时的取消通知.请注意,24小时与"一天"不同,因为日期因夏令时(DST)等异常而变化.

Instant twentyFourHoursEarlier = booking.minus ( 24 , ChronoUnit.HOURS );
Run Code Online (Sandbox Code Playgroud)

获取正在取消的用户的当前时刻.在这里,我们使用问题中指定的日期时间进行模拟.对于更短的代码,我们调整一小时,因为此解析方法仅处理UTC(Z)字符串.该问题在西班牙时间17:00表示; Europe/Madrid比UTC早1小时,所以我们减去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.
Run Code Online (Sandbox Code Playgroud)

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

Boolean cancelledEarlyEnough = cancellation.isBefore ( twentyFourHoursEarlier );
Run Code Online (Sandbox Code Playgroud)

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

Duration cancellationNotice = Duration.between ( cancellation , booking );
Run Code Online (Sandbox Code Playgroud)

转储到控制台.

System.out.println ( "zonedBooking: " + zonedBooking + " | booking: " + booking + " | twentyFourHoursEarlier: " + twentyFourHoursEarlier + " | cancellation: " + cancellation + " | cancelledEarlyEnough: " + cancelledEarlyEnough + " | cancellationNotice: " + cancellationNotice );
Run Code Online (Sandbox Code Playgroud)

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

通过应用他们期望/预期的时区向用户呈现.指定a Instant和a ZoneId来获取a 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 );
Run Code Online (Sandbox Code Playgroud)

让java.time通过指定短中长期标志和本地化为你Locale为人类的语言在其中(一)翻译日/月的名称,(b)利用文化规范的日期,时间部分的顺序,选择逗号与句号等等.

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

jueves 25 de febrero de 2016 16H00'CET

忽略服务器时区

您的问题提到了服务器的时区.你永远不应该依赖服务器的时区,因为它不受你作为程序员的控制,并且很容易被系统管理员改变.此外,JVM的当前默认时区可能基于主机操作系统的时区.但不一定.命令行启动JVM上的标志可以设置JVM的默认时区.更危险的是:JVM中任何应用程序的任何线程上的任何代码都可以设置时区并立即影响您的应用程序 - 在运行时!

如果未指定时区,则会隐式应用JVM的当前默认值.始终明确指定所需/预期的时区,如上面的代码所示.

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