将 XMLGregorianCalendar 转换为 LocalDateTime 时区不一致
Timezone inconsistencies on converting XMLGregorianCalendar to LocalDateTime
所以我有一个 XML 带有日期/时间字段的 Soap 响应,表示如下:
<BusStopTime>
<BusStopId>1023</BusStopId>
<Order>1</Order>
<PassingTime>1899-12-30T07:20:00</PassingTime>
</BusStopTime>
我对日期不感兴趣(因为这是一些我无法控制的遗留表示),但对时间感兴趣。该字段由 WS 工具转换为 XMLGregorianCalendar
,我打算进行转换。
var date = DatatypeFactory.newInstance()
.newXMLGregorianCalendar("1899-12-30T07:20:00")
.toGregorianCalendar().toInstant()
转换为 LocalDateTime
是 siLocalimple。我正在明确设置 TimeZone 以避免局部冲突
LocalDateTime.ofInstant(date, ZoneId.of("Europe/Warsaw"))
这导致 1899-12-30T07:44
LocalDateTime.ofInstant(date, ZoneId.of("Europe/Berlin"))
给我一个不同的输出 1899-12-30T07:20
当日期从现代开始时(1900 年之后及之后)- 一切正常。所以问题是:十九世纪初柏林和华沙之间到底发生了什么?或者说得更清楚——为什么时间的变化如此怪异?
我 运行 同时在 JDK8 和 JDK11 上(观察到相同的行为)
{ ~ } » java -version
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
LocalDateTime.parse()
如果您可以毫不费力地从 XML 中取出字符串,为了获得可预测的结果,请使用
LocalDateTime.parse("1899-12-30T07:20:00")
编辑: 如果不能直接访问字符串,我建议解决方案是在 XMLGregorianCalendar
上设置一个相对于 GMT/UTC 的偏移量以避免任何依赖JVM 的默认时区:
XMLGregorianCalendar xgc = DatatypeFactory.newInstance()
.newXMLGregorianCalendar("1899-12-30T07:20:00");
xgc.setTimezone(0);
LocalTime time = xgc.toGregorianCalendar()
.toZonedDateTime()
.toLocalTime();
System.out.println(time);
由于XMLGregorianCalendar
的所谓“时区”实际上只是一个固定的偏移量,所以我们设置哪个值并不重要。此代码段的输出始终是:
07:20
我测试了九个不同的默认时区,包括 Europe/Warsaw。
既然你说你只对一天中的时间感兴趣,对日期不感兴趣,我已经转换为LocalTime
。如果你想要 LocalDateTime
作为你的问题,只需使用 toLocalDateTime
而不是 toLocalTime
,
或者,这是您评论中的简单解决方案:
LocalDateTime.parse(xmllGregoriaCalendar.toXMLFormat())
toXMLFormat()
从创建 XMLGregorianCalendar
对象的 XML 重新创建字符串(文档保证您得到相同的字符串)。所以这种方式也避免了所有时区问题。
编辑:新旧之间的分歧classes
在我看来,问题的核心在于旧的和过时的 TimeZone
class 和现代的 ZoneId
class 不同意历史偏移GMT/UTC.
我做了几个实验。让我们首先尝试似乎工作正常的时区,柏林。从 1894 年到 1915 年,柏林的偏移量为 +01:00。Java 知道:
LocalDate baseDate = LocalDate.of(1899, Month.DECEMBER, 30);
ZoneId berlin = ZoneId.of("Europe/Berlin");
TimeZone tzb = TimeZone.getTimeZone(berlin);
GregorianCalendar gcb = new GregorianCalendar(tzb);
gcb.set(1899, Calendar.DECEMBER, 30);
ZonedDateTime zdtb = baseDate.atStartOfDay(berlin);
System.out.println("" + berlin + ' ' + tzb.getOffset(gcb.getTimeInMillis())
+ ' ' + berlin.getRules().getOffset(zdtb.toInstant())
+ ' ' + berlin.getRules().getOffset(zdtb.toInstant()).getTotalSeconds());
此片段的输出是:
Europe/Berlin 3600000 +01:00 3600
1899 年 12 月 30 日的偏移量正确为 +01:00。 TimeZone
class 说 3 600 000 毫秒,ZoneId
说 3600 秒,所以他们同意。
问题出在华沙。直到 1915 年,华沙一直处于 GMT 偏移 +01:24。让我们看看 Java 是否可以找到:
ZoneId warsaw = ZoneId.of("Europe/Warsaw");
TimeZone tzw = TimeZone.getTimeZone(warsaw);
GregorianCalendar gcw = new GregorianCalendar(tzw);
gcw.set(1899, Calendar.DECEMBER, 30);
ZonedDateTime zdtw = baseDate.atStartOfDay(warsaw);
System.out.println("" + warsaw + ' ' + tzw.getOffset(gcw.getTimeInMillis())
+ ' ' + warsaw.getRules().getOffset(zdtw.toInstant())
+ ' ' + warsaw.getRules().getOffset(zdtw.toInstant()).getTotalSeconds());
Europe/Warsaw 3600000 +01:24 5040
ZoneId
正确地表示 +01:24 或 5040 秒,但此处 TimeZone
表示 3 600 000 毫秒,与柏林的情况相同。这是不正确的。
旧的 GregorianCalendar
class 依赖于旧的 TimeZone
class 因此在使用 Europe/Warsaw 时区时会产生错误的结果(无论是明确的还是作为默认)。特别是你从 Calendar.toInstant()
得到了错误的 Instant
。正是因为 LocalDateTime.ofInstant
使用了现代的 ZoneId
,这个错误被带到了你的 LocalDateTime
.
同样来自 Europe/Dublin、Europe/Paris、Europe/Moscow 和 Asia/Kolkata 时区,我得到了相互矛盾的结果。
我在 Java 1.8.0_131、Java 9.0.4 和 Java 11 上有 运行 我的片段。结果是相同的所有版本。
链接
所以我有一个 XML 带有日期/时间字段的 Soap 响应,表示如下:
<BusStopTime>
<BusStopId>1023</BusStopId>
<Order>1</Order>
<PassingTime>1899-12-30T07:20:00</PassingTime>
</BusStopTime>
我对日期不感兴趣(因为这是一些我无法控制的遗留表示),但对时间感兴趣。该字段由 WS 工具转换为 XMLGregorianCalendar
,我打算进行转换。
var date = DatatypeFactory.newInstance()
.newXMLGregorianCalendar("1899-12-30T07:20:00")
.toGregorianCalendar().toInstant()
转换为 LocalDateTime
是 siLocalimple。我正在明确设置 TimeZone 以避免局部冲突
LocalDateTime.ofInstant(date, ZoneId.of("Europe/Warsaw"))
这导致 1899-12-30T07:44
LocalDateTime.ofInstant(date, ZoneId.of("Europe/Berlin"))
给我一个不同的输出 1899-12-30T07:20
当日期从现代开始时(1900 年之后及之后)- 一切正常。所以问题是:十九世纪初柏林和华沙之间到底发生了什么?或者说得更清楚——为什么时间的变化如此怪异?
我 运行 同时在 JDK8 和 JDK11 上(观察到相同的行为)
{ ~ } » java -version
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
LocalDateTime.parse()
如果您可以毫不费力地从 XML 中取出字符串,为了获得可预测的结果,请使用
LocalDateTime.parse("1899-12-30T07:20:00")
编辑: 如果不能直接访问字符串,我建议解决方案是在 XMLGregorianCalendar
上设置一个相对于 GMT/UTC 的偏移量以避免任何依赖JVM 的默认时区:
XMLGregorianCalendar xgc = DatatypeFactory.newInstance()
.newXMLGregorianCalendar("1899-12-30T07:20:00");
xgc.setTimezone(0);
LocalTime time = xgc.toGregorianCalendar()
.toZonedDateTime()
.toLocalTime();
System.out.println(time);
由于XMLGregorianCalendar
的所谓“时区”实际上只是一个固定的偏移量,所以我们设置哪个值并不重要。此代码段的输出始终是:
07:20
我测试了九个不同的默认时区,包括 Europe/Warsaw。
既然你说你只对一天中的时间感兴趣,对日期不感兴趣,我已经转换为LocalTime
。如果你想要 LocalDateTime
作为你的问题,只需使用 toLocalDateTime
而不是 toLocalTime
,
或者,这是您评论中的简单解决方案:
LocalDateTime.parse(xmllGregoriaCalendar.toXMLFormat())
toXMLFormat()
从创建 XMLGregorianCalendar
对象的 XML 重新创建字符串(文档保证您得到相同的字符串)。所以这种方式也避免了所有时区问题。
编辑:新旧之间的分歧classes
在我看来,问题的核心在于旧的和过时的 TimeZone
class 和现代的 ZoneId
class 不同意历史偏移GMT/UTC.
我做了几个实验。让我们首先尝试似乎工作正常的时区,柏林。从 1894 年到 1915 年,柏林的偏移量为 +01:00。Java 知道:
LocalDate baseDate = LocalDate.of(1899, Month.DECEMBER, 30);
ZoneId berlin = ZoneId.of("Europe/Berlin");
TimeZone tzb = TimeZone.getTimeZone(berlin);
GregorianCalendar gcb = new GregorianCalendar(tzb);
gcb.set(1899, Calendar.DECEMBER, 30);
ZonedDateTime zdtb = baseDate.atStartOfDay(berlin);
System.out.println("" + berlin + ' ' + tzb.getOffset(gcb.getTimeInMillis())
+ ' ' + berlin.getRules().getOffset(zdtb.toInstant())
+ ' ' + berlin.getRules().getOffset(zdtb.toInstant()).getTotalSeconds());
此片段的输出是:
Europe/Berlin 3600000 +01:00 3600
1899 年 12 月 30 日的偏移量正确为 +01:00。 TimeZone
class 说 3 600 000 毫秒,ZoneId
说 3600 秒,所以他们同意。
问题出在华沙。直到 1915 年,华沙一直处于 GMT 偏移 +01:24。让我们看看 Java 是否可以找到:
ZoneId warsaw = ZoneId.of("Europe/Warsaw");
TimeZone tzw = TimeZone.getTimeZone(warsaw);
GregorianCalendar gcw = new GregorianCalendar(tzw);
gcw.set(1899, Calendar.DECEMBER, 30);
ZonedDateTime zdtw = baseDate.atStartOfDay(warsaw);
System.out.println("" + warsaw + ' ' + tzw.getOffset(gcw.getTimeInMillis())
+ ' ' + warsaw.getRules().getOffset(zdtw.toInstant())
+ ' ' + warsaw.getRules().getOffset(zdtw.toInstant()).getTotalSeconds());
Europe/Warsaw 3600000 +01:24 5040
ZoneId
正确地表示 +01:24 或 5040 秒,但此处 TimeZone
表示 3 600 000 毫秒,与柏林的情况相同。这是不正确的。
旧的 GregorianCalendar
class 依赖于旧的 TimeZone
class 因此在使用 Europe/Warsaw 时区时会产生错误的结果(无论是明确的还是作为默认)。特别是你从 Calendar.toInstant()
得到了错误的 Instant
。正是因为 LocalDateTime.ofInstant
使用了现代的 ZoneId
,这个错误被带到了你的 LocalDateTime
.
同样来自 Europe/Dublin、Europe/Paris、Europe/Moscow 和 Asia/Kolkata 时区,我得到了相互矛盾的结果。
我在 Java 1.8.0_131、Java 9.0.4 和 Java 11 上有 运行 我的片段。结果是相同的所有版本。