Как десериализовать метку времени 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 2

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").

Используя приведенный выше пример, если вы видите значение value.getCreatedOn().getTime(), вы увидите, что это 1505296800000 - это число миллисекунд с момента эпоха (чтобы получить точность наносекунд, есть метод getNanos()).

Это значение миллисекунд соответствует 10 утра в UTC, 7 утра в Сан-Паулу, 11 утра в Лондоне, 7 вечера в Токио, 15:30 в Калькутте и так далее. Вы не преобразуете 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). Сериализация производит:

{"createdOn":"2017-09-13 15:30"}

Обратите внимание, что теперь выходные данные соответствуют местному времени в часовом поясе JVM по умолчанию (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;