有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

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

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

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

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

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

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

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


共 (3) 个答案

  1. # 1 楼答案

    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有效

  2. # 2 楼答案

    {a1}是正确的

    在UTC工作

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

    爪哇。时间

    一定要用java。Java 8及更高版本中内置的时间框架

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

    Local…类型是泛型的,不适用于任何地方,没有时区或offset-from-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 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.
    

    通过调用^{}^{}方法来比较Instant对象

    Boolean cancelledEarlyEnough = cancellation.isBefore ( twentyFourHoursEarlier );
    

    ADuration表示总秒数加上几分之一秒(纳秒)的时间跨度。我们用这个来帮助数学,验证我们得到了23小时的预期结果。{}方法使用{}的标准{a8},其中{}标记开始,{}将年-月-日部分与小时-分钟-秒部分分开,{}表示“小时”

    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[Europe/Madrid] | booking: 2016-02-25T15:00:00Z | twentyFourHoursEarlier: 2016-02-24T15:00:00Z | cancellation: 2016-02-24T16:00:00Z | cancelledEarlyEnough: false | cancellationNotice: PT23H

    通过应用所需/预期时区向用户演示。指定一个Instant和一个ZoneId来获取一个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。通过为人类语言指定一个短-中-长标志和一个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的当前默认值

  3. # 3 楼答案

    无论你身在何处,飞机都会准时起飞。这也称为时间戳。对于这种情况,存储时间戳将是一个合适的解决方案。在java中,当前时间戳可以通过System.currentTimeMillis()检索。此值不取决于服务器的时区,并且包含自1970年以来UTC的毫秒数

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

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

    您可能还希望使用joda time(它已被包含在InspiredJava v8 Java.time中)这样的库来处理日期和时间