Джексон (де) сериализация даты / времени Java8 клиентом JAX-RS

Я делаю Serivce-клиент для конечной точки REST, использую JAX-RS-клиент для HTTP-запросов и Джексона для (де) сериализации JSON-сущностей. Для обработки объектов даты / времени JSR-310 (Java8) я добавил com.fasterxml.jackson.datatype:jackson-datatype-jsr310 Модуль как зависимость от клиента службы, но я не получил его на работу.

Как настроить JAX-RS и / или Jackson для использования модуля jsr310?

Я использую следующие зависимости:

<dependency>
  <groupId>javax.ws.rs</groupId>
  <artifactId>javax.ws.rs-api</artifactId>
  <version>${jax-rs.version}</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-annotations</artifactId>
  <version>${jackson.version}</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
  <version>${jackson.version}</version>
</dependency>

Я не хочу, чтобы сервисный клиент (который выпускается как библиотека) зависел от какой-либо конкретной реализации - например, от Джерси, поэтому я полагаюсь только на API JAX-RS. Для запуска интеграционных тестов я добавил:

<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-client</artifactId>
  <version>${jersey.version}</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.inject</groupId>
  <artifactId>jersey-hk2</artifactId>
  <version>${jersey.version}</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.glassfish.jersey.media</groupId>
  <artifactId>jersey-media-json-jackson</artifactId>
  <version>${jersey.version}</version>
  <scope>test</scope>
</dependency>

Создание клиента JAX-RS выполняется на заводском объекте следующим образом:

import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;

@Produces
public Client produceClient() {
    return ClientBuilder.newClient();
}

Типичный DTO выглядит так:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;

import java.time.Instant;

import static java.util.Objects.requireNonNull;

@JsonPropertyOrder({"n", "t"})
public final class Message {

    private final String name;
    private final Instant timestamp;

    @JsonCreator
    public Message(@JsonProperty("n") final String name,
                   @JsonProperty("t") final Instant timestamp) {
        this.name = requireNonNull(name);
        this.timestamp = requireNonNull(timestamp);
    }

    @JsonProperty("n")
    public String getName() {
        return this.name;
    }

    @JsonProperty("t")
    public Instant getTimestamp() {
        return this.timestamp;
    }

    // equals(Object), hashCode() and toString()
}

Запросы выполняются так:

import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;

public final class Gateway {

    private final WebTarget endpoint;

    public Message postSomething(final Something something) {
        return this.endpoint
                .request(MediaType.APPLICATION_JSON_TYPE)
                .post(Entity.json(something), Message.class);
    }

    // where Message is the class defined above and Something is a similar DTO
}

JSON-сериализация и десериализация отлично работает для Strings, Ints, BigIntegers, Lists и т. Д. Однако, когда я делаю что-то вроде System.out.println(gateway.postSomthing(new Something("x", "y")); в моих тестах я получаю следующее исключение:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.Instant` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('Fri, 22 Sep 2017 10:26:52 GMT')
 at [Source: (org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream); line: 1, column: 562] (through reference chain: Message["t"])
        at org.example.com.ServiceClientTest.test(ServiceClientTest.java:52)
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('Fri, 22 Sep 2017 10:26:52 GMT')
 at [Source: (org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream); line: 1, column: 562] (through reference chain: Message["t"])
        at org.example.com.ServiceClientTest.test(ServiceClientTest.java:52)

Из чего я заключаю, что Джексон не знает, как десериализовать строки в инстансы. Я нашел блоги и ТАК вопросы по этой теме, но я не нашел четкого объяснения того, как заставить это работать.

Обратите внимание, что я бы хотел, чтобы клиент службы обрабатывал такие строки даты, как "Fri, 22 Sep 2017 10:26:52 GMT" так же как "2017-09-22T10:26:52.123Z", но я хочу, чтобы он всегда сериализовался в строки даты ISO 8601.

Кто может объяснить, как превратить десериализацию в Instant Работа?

2 ответа

В примере кода вы в настоящее время зависите от jersey-media-json-jackson. Вероятно, вам будет лучше, если вы будете использовать JSON JS-RS JSON от Джексона, поскольку вы можете настроить картограф Джексона с использованием стандартного API JAX-RS (и, конечно, API Джексона).

    <dependency>
        <groupId>com.fasterxml.jackson.jaxrs</groupId>
        <artifactId>jackson-jaxrs-json-provider</artifactId>
        <version>${jackson.version}</version>
    </dependency>

После удаления jersey-media-json-jackson и добавления зависимости jackson-jaxrs-json-provider вы можете настроить JacksonJaxbJsonProvider и зарегистрировать его в классе, который создает Client:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;

import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;

import static com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS;

public class ClientProducer {

    private JacksonJsonProvider jsonProvider;

    public ClientProducer() {
        // Create an ObjectMapper to be used for (de)serializing to/from JSON.
        ObjectMapper objectMapper = new ObjectMapper();
        // Register the JavaTimeModule for JSR-310 DateTime (de)serialization
        objectMapper.registerModule(new JavaTimeModule());
        // Configure the object mapper te serialize to timestamp strings.
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        // Create a Jackson Provider
        this.jsonProvider = new JacksonJaxbJsonProvider(objectMapper, DEFAULT_ANNOTATIONS);
    }

    @Produces
    public Client produceClient() {

        return ClientBuilder.newClient()
                // Register the jsonProvider
                .register(this.jsonProvider);
    }
}

Надеюсь это поможет.

Вы можете настроить Джексона ObjectMapper в ContextResolver, Поставщик Jackson JAX-RS найдет этот преобразователь и получит ObjectMapper от него.

@Provider
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {
    private final ObjectMapper mapper;

    public ObjectMapperResolver() {
        mapper = new ObjectMapper();
        // configure mapper
    }

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

Затем просто зарегистрируйте распознаватель, как любой другой поставщик или ресурс в своем приложении JAX-RS.

Другие вопросы по тегам