Android 设备错误地解析来自服务器的日期时间
An Android device parses datetime from a server incorrectly
我正在开发一个应用程序,其中一部分是向服务器发出 HTTP 请求,然后从 headers 解析服务器日期和时间。这是按以下方式完成的:
//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
变量会自动将时间调整到设备的时区(所以如果他得到的是格林威治标准时间晚上 21 点,而设备的时区是 GMT-2,那么输出将是晚上 19 点)。
现在这在我测试过的几台设备上工作得很好......除了一个。它总是产生一个比输入大两个小时 或 的结果(所以当服务器时间是晚上 21 点时,该函数将 return 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)上进行了测试,均产生了正确的结果。
如果有人知道可能导致此问题的原因,我将不胜感激!
Note that my SimpleDateFormat ignores the timezone in the parsed string. This is necessary in order to retain the original server time.
这是一个错误的假设,不是一个好的策略。这可能是您观察的原因。
如果不解析时区,您将依赖 SimpleDateFormat
的默认时区,在您的情况下就是设备的时区。由于 Date
对象和生成的时间戳均采用 UTC(总是!),因此您将根据设备的时区获得不同的时间戳。
既然你说设备处于另一个状态,那么时区可能不同。您只需更改其中一个工作设备的时区,您应该会看到相同的结果。
你是对的,你将不得不处理服务器和客户端时区之间的差异某处。只要您使用 Date
,就必须在某处对其进行格式化,可能会再次使用 SimpleDateFormat
。在该代码中,您将设置时区。
虽然这很麻烦,尤其是当您试图保留已解析的偏移量时。更好的方法是使用 OffsetDateTime
class from the java.time
package.. For Android, you'll need to use the JSR-310 Android Backport (ThreeTenABP) 库来利用这一点。
如 中所述,仅忽略偏移量 (GMT-8) 并不是一件好事,因为 SimpleDateFormat
将使用系统的默认时区 - 这个时区可能与每个device/environment,你无法控制它。即使默认时区是正确的,它也可以更改,即使在运行时也是如此,因此您不能假设它永远是您需要的。
示例:我的默认时区是 America/Sao_Paulo
,当前时差是 -03:00
(或 GMT-3
,或比 UTC 晚 3 小时)。如果我使用你的代码,它会假定它在圣保罗是 13:34(在 UTC 中是 16:34 - 毫秒值是 4876130061000
,这是错误的,因为它不等于原来的输入(GMT-8 中的13:34)。
如果我将默认时区更改为 GMT-8
,代码只会给出正确的值。不依赖于此,您可以设置格式化程序的时区。
SimpleDateFormat
has some patterns to parse timezone/offset,但是其中 none 与 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 GMT-8)。
另一个细节是,当您这样做时:
System.out.println(date);
它调用Date::toString
方法,该方法获取毫秒值并转换为系统默认时区,给人一种日期对象具有时区的错误印象 - but that's wrong:日期仅包含数字自 1970-01-01T00:00Z
.
以来的毫秒数
当您在调试器中看到日期时,通常也会调用 toString
如果你想打印某个特定时区的等效日期和时间值,你可以使用 SimpleDateFormat
并在此格式化程序中设置你想要的时区。使用 Date::toString
总是会误导您的价值观。
要使用服务器使用的相同偏移量打印 date/time,您可以创建一个 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
如果您不指定时区,它将使用 device/system 的默认值,根据设备的配置给出不同的结果。
请注意,我还使用了 Locale.ENGLISH
来解析输入。这是必需的,因为月份和星期几是英文的。如果我不指定 java.util.Locale
,格式化程序将使用系统的默认值,并且不能保证始终是英语。而且这也可以在运行时更改,因此最好使用显式而不是依赖默认值。
Java新Date/TimeAPI
旧的 classes(Date
、Calendar
和 SimpleDateFormat
)有 lots of problems and design issues,它们正在被新的 APIs.
在 Android 中你可以使用 ThreeTen Backport, a great backport for Java 8's new date/time classes. To make it work, you'll also need ThreeTenABP (more on how to use it ).
由于输入有日期、时间和偏移量,您可以使用 org.threeten.bp.format.DateTimeFormatter
.
将其解析为 org.threeten.bp.OffsetDateTime
一个细节是July 8th2124是星期六,所以我不得不更改输入字符串,否则会出错。 SimpleDateFormat
不会给出这个错误,因为众所周知它过于宽容并且会忽略很多错误并尝试以不太聪明的方式 "fix"(这可能有点基于意见,但很多认为这是一件坏事,这就是为什么新 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
class:
// 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
要转换为另一个时区或偏移量,您可以使用 classes org.threeten.bp.ZoneId
和 org.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
09/07/2124 02:34:21
08/07/2124 23:34:21
请注意,我使用了时区 Europe/Berlin
。
API 使用 IANA timezones names(格式总是 Continent/City
,如 America/Sao_Paulo
或 Europe/Berlin
)。
避免使用 3 个字母的缩写(如 CST
或 PST
),因为它们是 ambiguous and not standard.
您可以使用 ZoneId.getAvailableZoneIds()
.
获取所有可用时区的列表(并相应地选择)
我正在开发一个应用程序,其中一部分是向服务器发出 HTTP 请求,然后从 headers 解析服务器日期和时间。这是按以下方式完成的:
//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
变量会自动将时间调整到设备的时区(所以如果他得到的是格林威治标准时间晚上 21 点,而设备的时区是 GMT-2,那么输出将是晚上 19 点)。
现在这在我测试过的几台设备上工作得很好......除了一个。它总是产生一个比输入大两个小时 或 的结果(所以当服务器时间是晚上 21 点时,该函数将 return 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)上进行了测试,均产生了正确的结果。
如果有人知道可能导致此问题的原因,我将不胜感激!
Note that my SimpleDateFormat ignores the timezone in the parsed string. This is necessary in order to retain the original server time.
这是一个错误的假设,不是一个好的策略。这可能是您观察的原因。
如果不解析时区,您将依赖 SimpleDateFormat
的默认时区,在您的情况下就是设备的时区。由于 Date
对象和生成的时间戳均采用 UTC(总是!),因此您将根据设备的时区获得不同的时间戳。
既然你说设备处于另一个状态,那么时区可能不同。您只需更改其中一个工作设备的时区,您应该会看到相同的结果。
你是对的,你将不得不处理服务器和客户端时区之间的差异某处。只要您使用 Date
,就必须在某处对其进行格式化,可能会再次使用 SimpleDateFormat
。在该代码中,您将设置时区。
虽然这很麻烦,尤其是当您试图保留已解析的偏移量时。更好的方法是使用 OffsetDateTime
class from the java.time
package.. For Android, you'll need to use the JSR-310 Android Backport (ThreeTenABP) 库来利用这一点。
如 SimpleDateFormat
将使用系统的默认时区 - 这个时区可能与每个device/environment,你无法控制它。即使默认时区是正确的,它也可以更改,即使在运行时也是如此,因此您不能假设它永远是您需要的。
示例:我的默认时区是 America/Sao_Paulo
,当前时差是 -03:00
(或 GMT-3
,或比 UTC 晚 3 小时)。如果我使用你的代码,它会假定它在圣保罗是 13:34(在 UTC 中是 16:34 - 毫秒值是 4876130061000
,这是错误的,因为它不等于原来的输入(GMT-8 中的13:34)。
如果我将默认时区更改为 GMT-8
,代码只会给出正确的值。不依赖于此,您可以设置格式化程序的时区。
SimpleDateFormat
has some patterns to parse timezone/offset,但是其中 none 与 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 GMT-8)。
另一个细节是,当您这样做时:
System.out.println(date);
它调用Date::toString
方法,该方法获取毫秒值并转换为系统默认时区,给人一种日期对象具有时区的错误印象 - but that's wrong:日期仅包含数字自 1970-01-01T00:00Z
.
toString
如果你想打印某个特定时区的等效日期和时间值,你可以使用 SimpleDateFormat
并在此格式化程序中设置你想要的时区。使用 Date::toString
总是会误导您的价值观。
要使用服务器使用的相同偏移量打印 date/time,您可以创建一个 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
如果您不指定时区,它将使用 device/system 的默认值,根据设备的配置给出不同的结果。
请注意,我还使用了 Locale.ENGLISH
来解析输入。这是必需的,因为月份和星期几是英文的。如果我不指定 java.util.Locale
,格式化程序将使用系统的默认值,并且不能保证始终是英语。而且这也可以在运行时更改,因此最好使用显式而不是依赖默认值。
Java新Date/TimeAPI
旧的 classes(Date
、Calendar
和 SimpleDateFormat
)有 lots of problems and design issues,它们正在被新的 APIs.
在 Android 中你可以使用 ThreeTen Backport, a great backport for Java 8's new date/time classes. To make it work, you'll also need ThreeTenABP (more on how to use it
由于输入有日期、时间和偏移量,您可以使用 org.threeten.bp.format.DateTimeFormatter
.
org.threeten.bp.OffsetDateTime
一个细节是July 8th2124是星期六,所以我不得不更改输入字符串,否则会出错。 SimpleDateFormat
不会给出这个错误,因为众所周知它过于宽容并且会忽略很多错误并尝试以不太聪明的方式 "fix"(这可能有点基于意见,但很多认为这是一件坏事,这就是为什么新 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
class:
// 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
要转换为另一个时区或偏移量,您可以使用 classes org.threeten.bp.ZoneId
和 org.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
09/07/2124 02:34:21
08/07/2124 23:34:21
请注意,我使用了时区 Europe/Berlin
。
API 使用 IANA timezones names(格式总是 Continent/City
,如 America/Sao_Paulo
或 Europe/Berlin
)。
避免使用 3 个字母的缩写(如 CST
或 PST
),因为它们是 ambiguous and not standard.
您可以使用 ZoneId.getAvailableZoneIds()
.