【问题标题】:An Android device parses datetime from a server incorrectlyAndroid 设备错误地从服务器解析日期时间
【发布时间】:2018-01-20 11:25:07
【问题描述】:

我正在开发一个应用程序,其中一部分是向服务器发出 HTTP 请求,然后从标头中解析服务器日期和时间。这是通过以下方式完成的:

//The format used here typically looks like this: "Tue, 08 Jul 2124 13:34:21 GMT-8"
final SimpleDateFormat serverDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");
String string = response.headers.get("Date"); // this is where we get the Date and Time value
Date date = serverDateFormat.parse(string); // this is where we parse that string into the Date format
long result = date.getTime(); // this is where we translate it into millisec format
Log.e(TAG,"Date in milliseconds: "+result);

请注意,我的SimpleDateFormat 忽略解析字符串中的时区。这是为了保留原始服务器时间所必需的。如果我们解析时区,Date 变量会自动将时间调整为设备的时区(所以如果他得到了 GMT 晚上 21 点,并且设备的时区是 GMT-2,那么输出将是晚上 19​​ 点)。

现在,这在我测试过的几台设备上运行良好……除了一台。它总是产生一个比输入大一小时两个小时的结果(因此当服务器时间为下午 21 点时,该函数将返回下午 22 点 23 点)。由于该设备不属于我,并且处于完全不同的状态,因此获取 logcat 对我来说是有问题的。

老实说,这个问题对我来说毫无意义。我什至想不出在单个设备上不一致关闭时间的可能原因。

设备规格如下:Pixel XL、Android 7.1.2、PST 时区。该应用程序在 Nexus 5x(7.1.2,PST)、Galaxy S4(6.0.1,CST)、Galaxy S7(7.1.2,CST)上进行了测试,结果均正确。

如果有人知道可能导致此问题的原因,我将不胜感激!

【问题讨论】:

    标签: android http datetime timezone simpledateformat


    【解决方案1】:

    请注意,我的 SimpleDateFormat 忽略了解析字符串中的时区。这是为了保留原始服务器时间所必需的。

    这是一个错误的假设,不是一个好的策略。这可能是您观察的原因。

    通过不解析时区,您依赖的是SimpleDateFormat 的默认时区,在您的情况下将是设备的时区。由于 Date 对象和生成的时间戳都是以 UTC 表示的(总是!),因此您将根据设备的时区获得不同的时间戳。

    由于您说设备处于另一种状态,因此时区可能不同。您只需更改其中一个工作设备的时区,您应该会看到相同的结果。

    您是正确的,您将不得不处理服务器和客户端时区之间的差异某处。只要您使用Date,您就必须在某处对其进行格式化,可能再次使用SimpleDateFormat。在该代码中,您将设置时区。

    但这很麻烦,尤其是当您尝试保留已解析的偏移量时。更好的方法是使用OffsetDateTimefrom the java.time package.。对于 Android,您需要使用 JSR-310 Android Backport (ThreeTenABP) 库来利用此功能。

    另见:SimpleDateFormat Pitfalls

    【讨论】:

      【解决方案2】:

      正如@Matt's answer 中解释的那样,忽略偏移量 (GMT-8) 并不是一件好事,因为SimpleDateFormat 将使用系统的默认时区——并且该时区在每个设备/环境中可能不同,并且你无法控制它。即使默认时区是正确的,它也可以更改,即使在运行时也是如此,所以你不能假设它总是你需要的。

      示例:我的默认时区是America/Sao_Paulo,当前偏移量是-03:00(或GMT-3,或比UTC 晚3 小时)。如果我使用您的代码,它将假定它是圣保罗的 13:34(UTC 时间是 16:34 - millis 值是4876130061000,这是错误的,因为它不等于原始输入(13:34 in GMT-8))。

      如果我将默认时区更改为GMT-8,代码只会给出正确的值。为了不依赖于此,您可以设置格式化程序的时区。

      SimpleDateFormathas some patterns to parse timezone/offset,但它们都没有与GMT-8一起工作(似乎它只接受GMT-08:00),所以一种解决方案是从输入中删除它并使用它来设置正确的时区格式化程序:

      String input = "Tue, 08 Jul 2124 13:34:21 GMT-8";
      String[] v = input.split(" GMT"); // split the string, before and after "GMT"
      SimpleDateFormat serverDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH);
      // set the formatter timezone to the offset (in this case, to "GMT-8")
      serverDateFormat.setTimeZone(TimeZone.getTimeZone("GMT" + v[1]));
      // parse the date/time part
      Date date = serverDateFormat.parse(v[0]);
      

      日期毫秒值将为4876148061000,相当于2124-07-08T21:34:21Z(21:34 UTC = 13:34 in GMT-8)。


      另一个细节是,当你这样做时:

      System.out.println(date);
      

      它调用Date::toString 方法,该方法获取毫秒值并转换为系统的默认时区,给人以日期对象具有时区的错误印象 - but that's wrong:日期仅包含自1970-01-01T00:00Z 以来的毫秒数.

      toString 通常也会在您在调试器中看到日期时调用

      如果您想打印某些特定时区的等效日期和时间值,您可以使用SimpleDateFormat 并在此格式化程序中设置您想要的时区。使用 Date::toString 总是会误导您的价值观。

      要使用服务器使用的相同偏移量打印日期/时间,您可以创建一个SimpleDateFormat 并将GMT-8 设置为它,如上:

      // display the date in a specific format
      SimpleDateFormat outputFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
      // use the same offset used by the server (GMT-8)
      outputFormat.setTimeZone(TimeZone.getTimeZone("GMT-8"));
      System.out.println(outputFormat.format(date)); // 08/07/2124 13:34:21
      

      输出将是:

      08/07/2124 13:34:21

      如果您不指定时区,它将使用设备/系统的默认值,根据设备的配置给出不同的结果。

      请注意,我还使用Locale.ENGLISH 来解析输入。这是必需的,因为月份和星期几是英文的。如果我不指定java.util.Locale,格式化程序将使用系统的默认值,并且不保证始终为英文。而且这也可以在运行时更改,因此最好使用显式而不是依赖默认值。


      Java 新的日期/时间 API

      旧的类(DateCalendarSimpleDateFormat)有 lots of problemsdesign issues,它们正在被新的 API 取代。

      在 Android 中,您可以使用 ThreeTen Backport,这是 Java 8 新日期/时间类的一个很好的反向移植。为了使它工作,你还需要ThreeTenABP(更多关于如何使用它here)。

      由于输入具有日期、时间和偏移量,您可以使用org.threeten.bp.format.DateTimeFormatter 将其解析为org.threeten.bp.OffsetDateTime

      一个细节是 2124 年 7 月 8 日th 是星期六,所以我必须更改输入字符串,否则会出错。 SimpleDateFormat 没有给出这个错误,因为众所周知它过于宽容,忽略了很多错误,并尝试以不那么聪明的方式“修复”(这可能有点基于意见,但许多人认为这很糟糕这就是为什么新的 API 对此更加严格)。

      String input = "Sat, 08 Jul 2124 13:34:21 GMT-8";
      DateTimeFormatter parser = DateTimeFormatter.ofPattern("EE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH);
      OffsetDateTime odt = OffsetDateTime.parse(input, parser);
      

      odt 将等同于 2124-07-08T13:34:21-08:00。 无需在格式化程序中设置时区,因为它可以正确解析输入的偏移量。要获取毫秒值,只需执行以下操作:

      // the same  value as date.getTime()
      long millis = odt.toInstant().toEpochMilli();
      

      要转换为java.util.Date,您可以使用org.threeten.bp.DateTimeUtils 类:

      // convert to java.util.Date
      Date date = DateTimeUtils.toDate(odt.toInstant());
      

      要向用户显示,您可以使用不同的DateTimeFormatter。由于OffsetDateTime 保持正确的值,因此无需在格式化程序中设置时区:

      // display date in a specific format
      DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
      System.out.println(odt.format(outputFormat)); // 08/07/2124 13:34:21
      

      输出将是:

      08/07/2124 13:34:21

      要转换到另一个时区或偏移量,您可以使用 org.threeten.bp.ZoneIdorg.threeten.bp.ZoneOffset 类:

      // convert to UTC
      System.out.println(outputFormat.format(odt.withOffsetSameInstant(ZoneOffset.UTC)));
      // convert to offset +05:00
      System.out.println(outputFormat.format(odt.withOffsetSameInstant(ZoneOffset.ofHours(5))));
      // convert to Europe/Berlin timezone
      System.out.println(outputFormat.format(odt.atZoneSameInstant(ZoneId.of("Europe/Berlin"))));
      

      输出是:

      08/07/2124 21:34:21
      2124 年 9 月 7 日 02:34:21
      2124 年 8 月 7 日 23:34:21

      请注意,我使用了时区 Europe/Berlin。 API 使用IANA timezones names(始终采用Continent/City 格式,如America/Sao_PauloEurope/Berlin)。 避免使用三个字母的缩写(如CSTPST),因为它们是ambiguous and not standard

      您可以使用ZoneId.getAvailableZoneIds() 获取所有可用时区的列表(并进行相应选择)。

      【讨论】:

      • 很好的答案! :)
      • @MattJohnson 非常感谢!
      • 这是一个非常详细和全面的答案。非常感谢,我想我对日期如何运作的概念是错误的。非常感谢!
      猜你喜欢
      • 2020-08-06
      • 1970-01-01
      • 2018-05-27
      • 1970-01-01
      • 2015-11-19
      • 1970-01-01
      • 2018-06-18
      • 2022-01-13
      • 1970-01-01
      相关资源
      最近更新 更多