Как мне (не)упорядочить дату как отметку времени с Джексоном

У меня возникли проблемы с (не)сортировкой объектов java.util.Date в метки времени. В идеале метки времени должны быть в формате UTC-0, а не в локальном часовом поясе сервера. Хотя я могу легко обойти это, если мне нужно.

NB: я знаю, что есть несколько похожих тем о переполнении стека, но все, с которыми я сталкивался, либо устарели (в отношении используемого API), либо связаны с сериализацией объектов Date в строки.

Вот выдержка из моего файла POM:

<dependency>
    <groupId>javax.ws.rs</groupId>
    <artifactId>javax.ws.rs-api</artifactId>
    <version>2.0.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>2.15</version>
</dependency>

Пример класса модели:

public class PersonJSON {
    private Date birthDate;
}

Ожидаемый результат (при условии, что дата рождения 01 февраля 2015 г., 00:00:00 UTC-0):

{"birthDate":1422748800000}

Текущий выход:

{"birthDate":"2015-02-01"}

Можно ли это исправить (в идеале с помощью аннотации)?

Решение

Как оказалось, ошибка была вызвана неявным преобразованием из Java.sql.Date в Java.util.Date. Преобразование не вызывает исключений, а sql.Date расширяет util.Date, поэтому логически это должно работать, но это не так. Решение состояло в том, чтобы извлечь метку времени из Java.sql.Date и использовать ее в качестве входных данных для конструктора Java.util.Date.

Если вы хотите изменить формат, подходы, перечисленные peeskillet, верны, если у вас есть действительный объект Java.util.Date.


person Anders    schedule 11.02.2015    source источник


Ответы (2)


Вам просто нужно настроить SerializationFeature.WRITE_DATES_AS_TIMESTAMPS на ObjectMapper. Из того, что я тестировал, мне не нужно было настраивать десериализацию, она отлично работала, передавая метку времени.

@Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {

    private final ObjectMapper mapper;

    public ObjectMapperContextResolver() {
        mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) { return mapper; }

}

Что касается настройки этой функции с помощью аннотации для каждого поля, я не уверен, как это сделать с Джексоном без написания собственного сериализатора (но я не сомневаюсь, что есть какой-то другой выход). Вы всегда можете использовать поддержку аннотаций JAXB, которая поставляется с поставщиком Jackson. Просто напишите XmlAdapter и аннотируйте поле с помощью @XmlJavaTypeAdatper(YourDateAdapter.class). Вот пример


ОБНОВИТЬ

Полный пример.

Требуемые зависимости

<dependency>
    <groupId>org.glassfish.jersey.test-framework.providers</groupId>
    <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
    <version>2.15</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>2.15</version>
</dependency>

Тест (вам нужно зарегистрировать преобразователь контекста сверху)

import java.util.Date;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import jersey.stackoverflow.provider.ObjectMapperContextResolver;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

public class ObjectMapperTest extends JerseyTest {

    public static class Person {
        public Date birthDate;
    }

    @Path("/person") @Produces(MediaType.APPLICATION_JSON)
    public static class PersonResource {
        @GET 
        public Response getPerson() {
            Person person = new Person();
            person.birthDate = new Date();
            return Response.ok(person).build();
        }
    }

    @Override
    public Application configure() {
        return new ResourceConfig(PersonResource.class,
                                  ObjectMapperContextResolver.class);
    }

    @Test
    public void test() {
        String personJson = target("person").request().get(String.class);
        System.out.println(personJson);
    }
}

Результат: {"birthDate":1423738762437}


Обновление 2

Я не думаю, что этот ответ правильный. Кажется, Джексон уже сериализуется как временные метки по умолчанию. Если бы нам не нужны временные метки, мы бы установили приведенную выше конфигурацию как false. Но это все еще не отвечает на вопрос ОП. Не уверен, в чем проблема.

person Paul Samsotha    schedule 11.02.2015
comment
Могу ли я сохранить этот пользовательский провайдер/ContextResolver где-нибудь в пакете, содержащем остальное приложение, и тогда Джексон автоматически обнаружит его и будет использовать вместо реализации ContextResolver по умолчанию? Извините, если это глупый вопрос, но я никогда раньше не реализовывал пользовательские интерфейсы JAX-RS. - person Anders; 11.02.2015
comment
Если все ваши классы ресурсов подбираются при сканировании пакетов (это означает, что вы не регистрируете их явно), тогда да, этот поставщик должен подбираться автоматически (из-за аннотации @Provider). Просто поместите его в пакет на том же уровне или в подуровне пакета, который вы указали для сканирования. Если вы не указали путь, а ваши ресурсы все еще используются, то, вероятно, у вас сканируется весь путь к классам. В этом случае не имеет значения, в каком пакете он находится. И да, Джексон предоставляет MessageBodyReader, который вызовет этот ContextResolver. - person Paul Samsotha; 11.02.2015
comment
Хм. Когда я реализую пользовательский ContextResolver, как вы предложили, я ясно вижу, что метод и конструктор getContext вызываются для каждого запроса. Однако вывод JSON остается прежним. Тем не менее, это кажется правильным решением, поэтому я думаю, что отмечу вопрос как решенный. Спасибо :) - person Anders; 11.02.2015
comment
ContextResolver удаляет NULL_VALUES, как и ожидалось, когда я добавляю строку mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); так что это только объекты Date, с которыми он борется. Когда вы тестировали это, вы использовали тот же сервер приложений? Если вы использовали что-то еще, вы можете опубликовать свой POM или перечислить включенные библиотеки вашего сервера. Я начинаю задаваться вопросом, есть ли ошибка в одной из библиотек, включенных в мой проект. - person Anders; 11.02.2015
comment
Вы включили какие-либо фактические зависимости Джексона? Я видел, как люди случайно смешивали Jackson 1 и 2, и они использовали 1 аннотацию и 2 ObjectMapper, а затем удивлялись, почему аннотации не работали. Что касается помпона, у меня есть только jersey-media-json-jackson:2.15 и jersey-container-servlet:2.15. Это все, что вам нужно, чтобы запустить веб-приложение для майки. - person Paul Samsotha; 12.02.2015
comment
См. также [mapper.setSerializationInclusion(JsonInclude...)](fasterxml.github.io/jackson-databind/javadoc/2.2.0/com/. Вы можете настроить картограф вместо использования аннотаций - person Paul Samsotha; 12.02.2015
comment
И аннотации, и картограф работают для любого объекта, кроме объектов Date. Похоже, что-то происходит до того, как объект Date будет передан Джексону для обработки. - person Anders; 12.02.2015
comment
Кстати: Какой сервер приложений вы используете? Я мог бы попробовать посмотреть, есть ли конфликтующие библиотеки, включенные в мою. - person Anders; 12.02.2015
comment
Я только что запустил его в отдельном контейнере Grizzly. Я опубликую простой полный пример, чтобы показать, что это работает. - person Paul Samsotha; 12.02.2015
comment
Смотрите мое обновление. Просто запустите этот класс как тест JUnit. - person Paul Samsotha; 12.02.2015
comment
Знаешь что. Я только что обнаружил кое-что. Я думаю, что по умолчанию используется для использования временных меток. Я закомментировал конфигурацию, и она все равно отправила ее в виде метки времени. Думаю, тогда мой ответ неверен. Странный. Я мог бы поклясться вчера, что он так себя не вел. Может быть, я ошибся - person Paul Samsotha; 12.02.2015
comment
Не знаю, в чем тогда может быть проблема. Я подумал, может быть, используется другой провайдер, например MOXy (если вы используете Glassfish). Но вы говорите, что ваши аннотации Джексона работают. Вы используете много аннотаций? Мне просто интересно, может быть, конфигурация по умолчанию другого провайдера просто создает впечатление, что ваши аннотации работают. Например, MOXy имеет некоторые значения по умолчанию, такие как игнорирование нулей. Если вы находитесь в Glassfish, то MOXy будет использоваться по умолчанию, если вы не настроите другого провайдера. Как register(JacksonFeature.class). - person Paul Samsotha; 12.02.2015
comment
Я использую причал (-бегун). Он не содержит стандартной реализации трикотажа или MOXy/Jackson. Читая вики Джексона, я также читал, что временные метки были включены по умолчанию, но я просто предположил, что это было изменено в более поздней версии. Заметив, что никакие аннотации или изменения в конфигурации картографа не повлияли ни на один из объектов Date, я начал задаваться вопросом, не изменило ли что-то объекты перед отправкой их в картограф. Как я уже говорил ранее, все остальное работает независимо от того, устанавливаю ли я его через картограф или через аннотацию. - person Anders; 12.02.2015
comment
Да, я не уверен. Мне нужно было бы иметь возможность протестировать полный пример, демонстрирующий проблему. Я сомневаюсь, что какая-либо библиотека вызывает это. Можете ли вы предоставить достаточно информации, чтобы воспроизвести проблему. Что бы я сделал, так это создал новое приложение в самой простой форме, которое воспроизводит проблему. Иногда таким образом вы сами обнаруживаете проблему. когда вы получаете продукт, который воспроизводит проблему, если вы не можете ее понять, они публикуют все необходимые шаги для ее воспроизведения, т. е. конфигурацию приложения, зависимости, класс ресурсов, как вы вызываете сервер и т. д. Ключевое слово минимален. - person Paul Samsotha; 13.02.2015
comment
Вот как я на самом деле решаю большинство своих проблем :-) - person Paul Samsotha; 13.02.2015
comment
Проблема оказалась совершенно не связанной с аннотациями Джексона или ObjectMapperContextResolver. Я делал неявное преобразование из java.sql.Date в java.util.Date при установке даты рождения. Хотя sql.Date является специализацией util.Date, Джексон не может сериализовать его так же, как это было бы с java.util.Date, созданным с помощью конструктора. - person Anders; 13.02.2015

Я тоже столкнулся с этой проблемой. В моем случае Hibernate ORM загружал java.util.Date атрибуты сущности как java.sql.Date объекты, которые не сериализовались как временные метки, как я ожидал (что является стратегией сериализации по умолчанию, используемой Джексоном при сериализации java.util.Date объектов).

Мое исправление состояло в том, чтобы явно указывать java.sql.Date объектам использование стандартного класса com.fasterxml.jackson.databind.ser.std.DateSerializer Джексона точно так же, как он использует для объектов java.util.Date:

private Client buildClient() {
  return ClientBuilder.newBuilder()
    .register(getJacksonJsonProvider())
    .build();
}

private JacksonJsonProvider getJacksonJsonProvider() {
  JacksonJsonProvider jjp = new JacksonJaxbJsonProvider();
  jjp.setMapper(getJsonObjectMapper());
  return jjp;
}

private ObjectMapper getJsonObjectMapper() {
   ObjectMapper mapper = new ObjectMapper();
   mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
   SimpleModule module = new SimpleModule();
   module.addSerializer(java.sql.Date.class, new DateSerializer()); // <-- My Fix
   mapper.registerModule(module);
   return mapper;
}
person Brice Roncace    schedule 04.02.2016
comment
Спасибо. Интересно, есть ли более стандартный способ заставить Джексона относиться к дочернему классу как к отцовскому, но пока это работает для меня. - person John; 08.02.2017