41 votes

Sérialisation du format de date JSON de Jersey + Jackson - comment changer le format ou utiliser un JacksonJsonProvider personnalisé

J'utilise Jersey + Jackson pour fournir une couche de services REST JSON à mon application. Le problème que j'ai est que le format de sérialisation par défaut de la date ressemble à cela :

"CreationDate":1292236718456

J'ai d'abord pensé qu'il s'agissait d'un timestamp UNIX... mais il est trop long pour cela. Ma bibliothèque JS côté client a des problèmes pour désérialiser ce format (elle supporte un tas de formats de date différents mais pas celui-ci, je suppose). Je veux changer le format pour qu'il puisse être consommable par ma bibliothèque (en ISO par exemple). Comment puis-je faire cela ? J'ai trouvé un bout de code qui pourrait m'aider mais... où dois-je le mettre puisque je ne contrôle pas l'instanciation du sérialiseur Jackson (Jersey le fait) ?

objectMapper.configure(
    SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);

J'ai également trouvé ce code pour la personnalisation JacksonJsonProvider - La question est comment faire pour que toutes mes classes POJO l'utilisent ?

@Provider
public class MessageBodyWriterJSON extends JacksonJsonProvider {

    private static final String DF = "yyyy-MM-dd’T'HH:mm:ss.SSSZ";

    @Override
    public boolean isWriteable(Class arg0, Type arg1, Annotation[] arg2,
            MediaType arg3) {
        return super.isWriteable(arg0, arg1, arg2,
                arg3);
    }
    @Override
    public void writeTo(Object target, Class arg1, Type arg2, Annotation[] arg3,
            MediaType arg4, MultivaluedMap arg5, OutputStream outputStream)
            throws IOException, WebApplicationException {
            SimpleDateFormat sdf=new SimpleDateFormat(DF);

        ObjectMapper om = new ObjectMapper();
        om.getDeserializationConfig().setDateFormat(sdf);
        om.getSerializationConfig().setDateFormat(sdf);
        try {
            om.writeValue(outputStream, target);
        } catch (JsonGenerationException e) {
            throw e;
        } catch (JsonMappingException e) {
            throw e;
        } catch (IOException e) {
            throw e;
        }
    }
}

0 votes

J'ai trouvé que c'était un changement entre Jersey 1.1.5 et Jersey 1.6 - avec Jersey 1.1.5, la sérialisation JSON ressemble à ceci : {"date":"2011-04-01T16:41:18.707+00:00"} - il y a peut-être un article de l'Issue Tracker qui donne des informations de base.

0 votes

Oui, c'est la plomberie et l'installation qui prennent le plus de temps. Je n'ai toujours pas trouvé la solution. Je suggère de sortir les dates sous forme de chaîne et de laisser votre client se charger de convertir les chaînes en dates. Il s'agit de substituer le code à la configuration. Je préfère écrire du code qui est "sous mon contrôle" plutôt que des mystères de configuration que je sais pouvoir contrôler mais qui semblent pour l'instant hors de mon contrôle. Les configurations sont délicates sans exemples explicites qui se trouvent utiliser les mêmes éléments de la boîte à outils Java que vous utilisez. La portabilité est un objectif louable mais vraiment difficile.

35voto

Riccardo Cossu Points 1465

J'ai réussi à le faire dans Resteasy "à la manière de JAX-RS", donc cela devrait fonctionner sur toute implémentation conforme comme Jersey (récemment testé avec succès sur le serveur JEE7 Wildfly 8, cela a juste nécessité quelques changements dans la partie Jackson parce qu'ils ont changé quelques API).

Vous devez définir un ContextResolver (vérifiez que Produces contient le bon type de contenu) :

import javax.ws.rs.ext.ContextResolver;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.DeserializationConfig;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.Produces; 
import java.text.SimpleDateFormat;
@Provider
@Produces("application/json")
public class JacksonConfigurator implements ContextResolver<ObjectMapper> {

    private ObjectMapper mapper = new ObjectMapper();

    public JacksonConfigurator() {
        SerializationConfig serConfig = mapper.getSerializationConfig();
        serConfig.setDateFormat(new SimpleDateFormat(<my format>));
        DeserializationConfig deserializationConfig = mapper.getDeserializationConfig();
        deserializationConfig.setDateFormat(new SimpleDateFormat(<my format>));
        mapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
    }

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

}

Vous devez ensuite renvoyer la classe nouvellement créée dans la fonction getClasses de votre application javax.ws.rs.core.Application.

import javax.ws.rs.core.Application;
public class RestApplication extends Application {

     @Override
     public Set<Class<?>> getClasses() {
         Set<Class<?>> classes = new HashSet<Class<?>>();
         // your classes here
         classes.add(JacksonConfigurator.class);
         return classes;
      }

}

De cette façon, toutes les opérations effectuées par jackson reçoivent l'ObjectMapper de votre choix.

EDIT : J'ai récemment découvert à mes frais qu'en utilisant RestEasy 2.0.1 (et donc Jackson 1.5.3) il y a un comportement étrange si vous décidez d'étendre le JacksonConfigurator pour ajouter des mappings personnalisés.

import javax.ws.rs.core.MediaType;
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class MyJacksonConfigurator extends JacksonConfigurator

Si vous faites comme ça (et bien sûr mettez la classe étendue dans RestApplication) le mappeur de la classe parente est utilisé, c'est-à-dire que vous perdez les mappings personnalisés. Pour que cela fonctionne correctement, j'ai dû faire quelque chose qui me semble inutile autrement :

public class MyJacksonConfigurator extends JacksonConfigurator implements ContextResolver<ObjectMapper>

10 votes

SimpleDateFormat ne convient pas aux environnements multithreads (j'ai vu que cela produisait de méchants bogues). Quelqu'un sait-il comment la configuration utilise le SimpleDataFormat ?

11 votes

Oui, je le sais (bien que je sois toujours surpris de constater combien de développeurs "expérimentés" ne le savent pas) ; j'ai regardé à la fois dans la documentation et dans le code, et l'instance fournie n'est utilisée que comme modèle, elle est toujours clonée avant d'être utilisée. Je ne suis pas sûr que ce soit un excellent choix de la part des développeurs de Jackson, mais leur API nécessite un DateFormat, dont la sécurité threadsafe n'est pas garantie, et ils en tiennent compte en les clonant. Merci quand même pour votre bon commentaire.

1 votes

J'ai essayé mais ni getClasses() ni getContext() ne sont appelés dans mon projet. Avez-vous des conseils sur ce que je peux faire de mal ? Y a-t-il quelque chose dans le fichier web.xml qui doit être défini également ?

16voto

Mark Points 1106

Pour configurer votre propre ObjectMapper, vous devez injecter votre propre classe qui implémente ContextResolver<ObjectMapper>.

La manière exacte de faire en sorte que Jersey reprenne cela dépendra de votre CIO (spring, guice). J'utilise spring, et ma classe ressemble à quelque chose comme ça :

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig.Feature;
import org.codehaus.jackson.map.deser.CustomDeserializerFactory;
import org.codehaus.jackson.map.deser.StdDeserializerProvider;
import org.codehaus.jackson.map.ser.CustomSerializerFactory;
import org.springframework.stereotype.Component;

// tell spring to look for this.
@Component
// tell spring it's a provider (type is determined by the implements)
@Provider
public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
    @Override
    public ObjectMapper getContext(Class<?> type) {
        // create the objectMapper.
        ObjectMapper objectMapper = new ObjectMapper();
        // configure the object mapper here, eg.
           objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
        return objectMapper;
    }
}

6voto

StaxMan Points 34626

Pour ce que ça vaut, ce nombre est l'horodatage standard de Java (utilisé par les classes du JDK) ; Unix stocke les secondes, Java les millisecondes, c'est pourquoi c'est une valeur un peu plus grande.

J'espère qu'il existe des documents sur la manière d'injecter ObjectMapper dans Jersey (cela devrait suivre la manière habituelle d'injecter un objet fourni). Mais vous pourriez aussi surcharger JacksonJaxRsProvider pour spécifier/configurer ObjectMapper et l'enregistrer ; c'est ce que Jersey fait elle-même, et il y a plusieurs façons de le faire.

4 votes

Merci beaucoup pour l'astuce. Le problème est que je n'arrive pas à trouver de documentation sur la façon de procéder.

1voto

Ignacio Rubio Points 549

J'ai eu le même problème (en utilisant Jersey+Jackson+Json), le client envoyait une date, mais elle était modifiée dans le serveur lorsque les données étaient mappées dans l'objet.

J'ai suivi une autre approche pour résoudre ce problème, en lisant ce lien : http://blog.bdoughan.com/2010/07/xmladapter-jaxbs-secret-weapon.html quand j'ai réalisé que la date reçue était un TimeStamp (le même qu'Adrin dans sa question : "creationDate":1292236718456 )

Dans ma classe VO, j'ai ajouté cette annotation à l'attribut @XmlJavaTypeAdapter et a également implémenté une classe interne qui étend XmlAdapter :

@XmlRootElement
public class MyClassVO {
   ...
   @XmlJavaTypeAdapter(DateFormatterAdapter.class) 
   Date creationDate;
   ...

   private static class DateFormatterAdapter extends XmlAdapter<String, Date> {
      @Override
      public Date unmarshal(final String v) throws Exception {
         Timestamp stamp = new Timestamp(new Long(v));
         Date date = new Date(stamp.getTime());
         return date;
      }
}

J'espère que cela pourra vous aider aussi.

-1voto

user699634 Points 29

Réécrivez le MessageBodyWriterJSON avec ceci

import javax.ws.rs.core.MediaType; 
import javax.ws.rs.ext.Provider; 

import org.codehaus.jackson.jaxrs.JacksonJsonProvider; 
import org.codehaus.jackson.map.ObjectMapper; 
import org.codehaus.jackson.map.SerializationConfig; 

@Provider 
public class MessageBodyWriterJSON extends JacksonJsonProvider { 
            public MessageBodyWriterJSON (){ 
            } 

        @Override 
            public ObjectMapper locateMapper(Class<?> type, MediaType mediaType) 
        { 
        ObjectMapper mapper = super.locateMapper(type, mediaType); 
        //DateTime in ISO format "2012-04-07T17:00:00.000+0000" instead of 'long' format 
            mapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false); 
            return mapper; 
        } 
}

1 votes

Alors quoi ? Comment faire en sorte que Jersey utilise ça ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X