Как десериализовать метку времени as-IS с помощью аннотации Джексона в Java?
У меня есть поле в моем Java Bean, как показано ниже, где @JsonFormat
является аннотацией Джексона:
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm")
private Timestamp createdOn;
Когда я возвращаю Timestamp
, он всегда преобразуется в UTC время в моем ответе JSON. Допустим, при форматировании моей временной метки я получаю 2017-09-13 15:30
, но мой ответ возвращается как 2017-09-13 10:00
.
Я знаю, что это из-за аннотации Джексона по умолчанию принимает системный часовой пояс и преобразует метку времени в UTC. (В моем случае сервер принадлежит к часовому поясу Asia/Calcutta
, и поэтому смещение равно +05:30
и картограф Джексона вычитает 5 часов и 30 минут, чтобы преобразовать метку времени в UTC)
2017-09-13 15:30
вместо 2017-09-13 10:00
?2 ответа:
Я провел здесь тест, изменив часовой пояс JVM по умолчанию на
Asia/Calcutta
и создав класс с полемjava.sql.Timestamp
:public class TestTimestamp { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm") private Timestamp createdOn; // getter and setter }
В вашем другом вопросе вы сказали, что используется JDK 8, поэтому я тестировал в той же версии (если вы используете другую версию (JDK "Не Java 8" внизу). Сначала я создал
Timestamp
, который соответствует 13 сентября th 2017, в 10 утра по UTC :TestTimestamp value = new TestTimestamp(); // timestamp corresponds to 2017-09-13T10:00:00 UTC value.setCreatedOn(Timestamp.from(Instant.parse("2017-09-13T10:00:00Z")));
Затем я сериализовал его с помощью Джексона
ObjectMapper
:ObjectMapper om = new ObjectMapper(); String s = om.writeValueAsString(value);
Результат
String
таков:{"createdOn":"2017-09-13 10:00"}
Обратите внимание, что в сгенерированном JSON выходные данные находятся в UTC (10 AM). Если я десериализовать этот JSON:
value = om.readValue(s, TestTimestamp.class); System.out.println(value.getCreatedOn());
Это выведет:
2017-09-13 15:30:00.0
Это потому, что метод
Как я объяснил в своем ответе на ваш другой вопрос, объектTimestamp::toString()
(который неявно вызывается вSystem.out.println
) печатает метку времени в часовом поясе по умолчанию JVM. В этом случае значение по умолчанию равноAsia/Calcutta
, и 10 утра в UTC совпадает с 15:30 в Калькутте, поэтому вывод выше произведен.Timestamp
не имеет никакой информации о часовом поясе. Он имеет только число наносекунд с момента unix эпохи (1970-01-01T00:00Z
или "1 января 1970 года в полночь по UTC").Используя приведенный выше пример, если вы видите значение
Это значение миллисекунд соответствует 10 утра в UTC, 7 утра в Сан-Паулу, 11 утра в Лондоне, 7 вечера в Токио, 15:30 в Калькутте и так далее. Вы не преобразуетеvalue.getCreatedOn().getTime()
, вы увидите, что это1505296800000
- это число миллисекунд с момента эпоха (чтобы получить точность наносекунд, есть методgetNanos()
).Timestamp
между зонами, потому что значение millis одинаково везде в мире. Однако вы можете изменить представлениеэтого значения (соответствующей даты/времени в определенном часовом поясе).В Джексоне вы можете создайте пользовательские сериализаторы и десериализаторы (расширяя
com.fasterxml.jackson.databind.JsonSerializer
иcom.fasterxml.jackson.databind.JsonDeserializer
), чтобы иметь больше контроля над тем, как вы форматируете и анализируете даты.Сначала я создаю сериализатор, который форматирует
Timestamp
в часовой пояс JVM по умолчанию:public class TimestampSerializer extends JsonSerializer<Timestamp> { private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @Override public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { // get the timestmap in the default timezone ZonedDateTime z = value.toInstant().atZone(ZoneId.systemDefault()); String str = fmt.format(z); gen.writeString(str); } }
Затем я создаю десериализатор, который считывает дату в часовом поясе JVM по умолчанию и создает
Timestamp
:public class TimestampDeserializer extends JsonDeserializer<Timestamp> { private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @Override public Timestamp deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException { // parse to a LocalDateTime LocalDateTime dt = LocalDateTime.parse(jsonParser.getText(), fmt); // the date/time is in the default timezone return Timestamp.from(dt.atZone(ZoneId.systemDefault()).toInstant()); } }
Я также изменяю поле, чтобы использовать эти пользовательские классы:
// remove JsonFormat annotation @JsonSerialize(using = TimestampSerializer.class) @JsonDeserialize(using = TimestampDeserializer.class) private Timestamp createdOn;
Теперь, при использовании того же самого
Timestamp
выше (что соответствует2017-09-13T10:00:00Z
). Сериализация производит:Обратите внимание, что теперь выходные данные соответствуют местному времени в часовом поясе JVM по умолчанию (15:30 в{"createdOn":"2017-09-13 15:30"}
Asia/Calcutta
).При десериализации этого JSON я получаю обратно то же самое
Timestamp
(соответствует 10 утра в UTC).
Этот код использует часовой пояс JVM по умолчанию, но его можно изменить без предварительного уведомления, даже во время выполнения , поэтому лучше всегда делать это уточните, какой из них вы используете.
API используетимена часовых поясов IANA (всегда в формате
Вы можете получить список доступных часовых поясов (и выбрать тот, который лучше всего подходит вашей системе), позвонив по телефонуRegion/City
, напримерAsia/Calcutta
илиEurope/Berlin
), поэтому вы можете создать их с помощьюZoneId.of("Asia/Calcutta")
. Избегайте использования 3-буквенных аббревиатур (например,IST
илиPST
), поскольку они являются неоднозначными и не стандартными.ZoneId.getAvailableZoneIds()
.Если вы хотите изменить выходные данные, чтобы они соответствовали другим часовой пояс, просто измените
ZoneId.systemDefault()
на нужную вам зону. Просто имейте в виду, что одна и та же зона должна использоваться как для сериализации, так и для десериализации, иначе вы получите неправильные результаты.
Не Java 8?
Если вы используете Java Threeten Backport, отличный backport для новых классов даты и времени Java 8.
Единственное различие-это имена пакетов (в Java 8 этоjava.time
, а в ThreeTen Backport этоorg.threeten.bp
), но классы и методы имена одни и те же.Есть и другое отличие: только в Java 8 класс
Timestamp
имеет методыtoInstant()
иfrom()
, поэтому вам нужно будет использовать классorg.threeten.bp.DateTimeUtils
для выполнения преобразований:public class TimestampSerializer extends JsonSerializer<Timestamp> { private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @Override public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { // get the timestmap in the default timezone ZonedDateTime z = DateTimeUtils.toInstant(value).atZone(ZoneId.systemDefault()); String str = fmt.format(z); gen.writeString(str); } } public class TimestampDeserializer extends JsonDeserializer<Timestamp> { private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @Override public Timestamp deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException { // parse to a LocalDateTime LocalDateTime dt = LocalDateTime.parse(jsonParser.getText(), fmt); // the date/time is in the default timezone return DateTimeUtils.toSqlTimestamp(dt.atZone(ZoneId.systemDefault()).toInstant()); } }
Вы можете использовать
timeZone
чтобы явно указать нужный часовой пояс:@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm", timezone="Asia/Calcutta") private Timestamp createdOn;