Несоответствия часовых поясов при преобразовании 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
это просто Я устанавливаю TimeZone явно, чтобы избежать путаницы
LocalDateTime.ofInstant(date, ZoneId.of("Europe/Warsaw"))
что приводит к 1899-12-30T07:44
LocalDateTime.ofInstant(date, ZoneId.of("Europe/Berlin"))
дает мне другой вывод 1899-12-30T07:20
Когда даты начинаются в наши дни (после 1900 года и после) - все работает отлично. Итак, вопрос: что именно произошло между Берлином и Варшавой на рубеже XIX века? Или, если выразиться более четко - почему изменение во времени так странно?
Я бегу на обоих 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)
1 ответ
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
на самом деле ничего, кроме фиксированного смещения, не имеет значения, какое значение мы устанавливаем. Результат из этого фрагмента:
7:20
Я проверил с девятью различными часовыми поясами по умолчанию, включая Европу / Варшаву.
Поскольку вы сказали, что вас интересует только время суток, а не дата, я перевел на LocalTime
, Если вы хотите LocalDateTime
как в вашем вопросе, просто используйте toLocalDateTime
вместо toLocalTime
,
Кроме того, это было ваше простое решение из вашего комментария:
LocalDateTime.parse(xmllGregoriaCalendar.toXMLFormat())
toXMLFormat()
воссоздает строку из XML, что XMLGregorianCalendar
объект был создан из (документы гарантируют, что вы получите ту же строку обратно). Таким образом, этот способ также избегает всех проблем с часовыми поясами.
Редактировать: разногласия между старым и новым классами
Мне кажется, что суть проблемы лежит в старом и устаревшем TimeZone
класс и современный ZoneId
класс не согласен с историческими смещениями из GMT/UTC.
Я сделал пару экспериментов. Давайте сначала попробуем часовой пояс, который, кажется, работает правильно, Берлин. Берлин был по смещению +01:00 с 1894 по 1915 год. Ява знает, что:
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
Смещение за 30 декабря 1899 года правильно указано как +01:00. TimeZone
класс говорит 3 600 000 миллисекунд, ZoneId
говорит 3600 секунд, поэтому они согласны.
Беда с Варшавой. Варшава все время находилась в смещении по Гринвичу +01:24 до 1915 года. Посмотрим, сможет ли 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
класс и, следовательно, дает неправильные результаты при использовании часового пояса Европа / Варшава (либо явно, либо по умолчанию). В частности, вы ошибаетесь Instant
от Calendar.toInstant()
, И именно потому, что LocalDateTime.ofInstant
использует современный ZoneId
, ошибка переносится в ваш LocalDateTime
,
Также из часовых поясов Европы / Дублина, Европы / Парижа, Европы / Москвы и Азии / Калькутты я получаю противоречивые результаты.
Я запустил свои фрагменты на Java 1.8.0_131, Java 9.0.4 и Java 11. Результаты были одинаковыми для всех версий.
связи
- Документация
DatatypeFactory.newXMLGregorianCalendar(String)
- Часовой пояс в Берлине, Германия на timeanddate.com
- Часовой пояс в Варшаве, Польша (Warszawa) на timeanddate.com