首页 > 解决方案 > 将 XMLGregorianCalendar 转换为 LocalDateTime 时区不一致

问题描述

所以我有一个带有日期/时间字段的 XML Soap 响应,表示如下:

<BusStopTime>
    <BusStopId>1023</BusStopId>
    <Order>1</Order>
    <PassingTime>1899-12-30T07:20:00</PassingTime>
</BusStopTime>

我对日期不感兴趣(因为这是我无法控制的一些遗留表示),而是时间。该字段XMLGregorianCalendar由 WS 工具转换为,我的目标是进行转换。

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)

标签: javadatetimetimetimezonexmlgregoriancalendar

解决方案


LocalDateTime.parse()

如果您可以毫不费力地从 XML 中获取字符串,那么为了获得可预测的结果,请使用

    LocalDateTime.parse("1899-12-30T07:20:00")

编辑:在没有直接访问字符串的情况下,我建议解决方案是在您的 GMT/UTC 上设置偏移量,XMLGregorianCalendar以避免对 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

我已经测试了九个不同的默认时区,包括欧洲/华沙。

既然你说你只对一天中的时间感兴趣,而不是日期,我已经转换为LocalTime. 如果你想LocalDateTime在你的问题中使用 as ,只需使用toLocalDateTime而不是toLocalTime,

或者,这是您评论中的简单解决方案:

    LocalDateTime.parse(xmllGregoriaCalendar.toXMLFormat​())

toXMLFormat​()从创建对象的 XML 中重新创建字符串XMLGregorianCalendar(文档保证您得到相同的字符串)。所以这种方式也能规避所有时区问题。

编辑:新旧类之间的分歧

在我看来,问题的核心在于旧的和过时TimeZone的类和现代ZoneId类不同意与 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());

此片段的输出是:

欧洲/柏林 3600000 +01:00 3600

1899 年 12 月 30 日的偏移量正确地给出为 +01:00。TimeZone班级说 3 600 000 毫秒,说ZoneId3600 秒,所以他们同意。

麻烦在于华沙。直到 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());

欧洲/华沙 3600000 +01:24 5040

ZoneId正确地说 +01:24 或 5040 秒,但这里TimeZone说的是 3 600 000 毫秒,与柏林的情况相同。这是不正确的。

GregorianCalendar类依赖于旧TimeZone类,因此在使用欧洲/华沙时区(显式或默认)时会产生错误的结果。特别是你InstantCalendar.toInstant(). 而正是因为LocalDateTime.ofInstant使用了现代ZoneId的,错误被进行到了你的LocalDateTime.

同样来自欧洲/都柏林、欧洲/巴黎、欧洲/莫斯科和亚洲/加尔各答时区,我得到了相互矛盾的结果。

我在 Java 1.8.0_131、Java 9.0.4 和 Java 11 上运行了我的代码片段。所有版本的结果都相同。

链接


推荐阅读