J'ai une spécification d'API REST qui parle avec les microservices back-end, qui renvoient les valeurs suivantes:

Sur les réponses "collections" (par exemple GET / utilisateurs):

{
    users: [
        {
            ... // single user object data
        }
    ],
    links: [
        {
            ... // single HATEOAS link object
        }
    ]
}

Pour les réponses "objet unique" (par exemple GET /users/{userUuid}):

{
    user: {
        ... // {userUuid} user object}
    }
}

Cette approche a été choisie pour que les réponses uniques soient extensibles (par exemple, si GET /users/{userUuid} obtient un paramètre de requête supplémentaire en bas de la ligne, comme ?detailedView=true, nous aurions des informations de requête supplémentaires).

Fondamentalement, je pense que c'est une approche correcte pour minimiser les changements de rupture entre les mises à jour de l'API. Cependant, traduire ce modèle en code s'avère très ardu.

Disons que pour les réponses uniques, j'ai l'objet de modèle d'API suivant pour un seul utilisateur:

public class SingleUserResource {
    private MicroserviceUserModel user;

    public SingleUserResource(MicroserviceUserModel user) {
        this.user = user;
    }

    public String getName() {
        return user.getName();
    }

    // other getters for fields we wish to expose
}

L'avantage de cette méthode est que nous pouvons exposer uniquement les champs des modèles utilisés en interne pour lesquels nous avons des getters publics, mais pas d'autres. Ensuite, pour les réponses de collections, j'aurais la classe wrapper suivante:

public class UsersResource extends ResourceSupport {

    @JsonProperty("users")
    public final List<SingleUserResource> users;

    public UsersResource(List<MicroserviceUserModel> users) {
        // add each user as a SingleUserResource
    }
}

Pour les réponses objet unique , nous aurions les éléments suivants:

public class UserResource {

    @JsonProperty("user")
    public final SingleUserResource user;

    public UserResource(SingleUserResource user) {
        this.user = user;
    }
}

Cela donne JSON réponses qui sont formatées selon la spécification de l'API en haut de cet article. L'avantage de cette approche est que nous n'exposons que les champs que nous voulons exposer. Le gros inconvénient est que j'ai une tonne de classes wrapper qui n'effectuent aucune tâche logique perceptible en dehors d'être lues par Jackson pour produire une réponse correctement formatée.

Mes questions sont les suivantes:

  • Comment puis-je généraliser cette approche? Idéalement, j'aimerais avoir une seule classe BaseSingularResponse (et peut-être une classe BaseCollectionsResponse extends ResourceSupport) que tous mes modèles peuvent étendre, mais vu comment Jackson semble dériver les clés JSON des définitions d'objet, je voudrais devez utiliser quelque chose comme Javaassist pour ajouter des champs aux classes de réponse de base à Runtime - un hack sale dont je voudrais rester aussi loin que possible humainement.

  • Existe-t-il un moyen plus simple de réaliser cela? Malheureusement, je peux avoir un nombre variable d'objets JSON de niveau supérieur dans la réponse dans un an, donc je ne peux pas utiliser quelque chose comme celui de Jackson SerializationConfig.Feature.WRAP_ROOT_VALUE car cela englobe tout dans une seule racine- objet de niveau (pour autant que je sache).

  • Y a-t-il peut-être quelque chose comme @JsonProperty pour le niveau de la classe (par opposition uniquement au niveau de la méthode et du champ)?

17
user991710 26 janv. 2017 à 21:37

4 réponses

Meilleure réponse

Il existe plusieurs possibilités.

Vous pouvez utiliser un java.util.Map:

List<UserResource> userResources = new ArrayList<>();
userResources.add(new UserResource("John"));
userResources.add(new UserResource("Jane"));
userResources.add(new UserResource("Martin"));
Map<String, List<UserResource>> usersMap = new HashMap<String, List<UserResource>>();
usersMap.put("users", userResources);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(usersMap));

Vous pouvez utiliser ObjectWriter pour envelopper la réponse que vous pouvez utiliser comme ci-dessous:

ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
result = writer.writeValueAsString(object);

Voici une proposition pour généraliser cette sérialisation.

Une classe pour gérer un objet simple :

public abstract class BaseSingularResponse {

    private String root;

    protected BaseSingularResponse(String rootName) {
        this.root = rootName;
    }

    public String serialize() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().withRootName(root);
        String result = null;
        try {
            result = writer.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            result = e.getMessage();
        }
        return result;
    }
}

Une classe pour gérer la collection :

public abstract class BaseCollectionsResponse<T extends Collection<?>> {
    private String root;
    private T collection;

    protected BaseCollectionsResponse(String rootName, T aCollection) {
        this.root = rootName;
        this.collection = aCollection;
    }

    public T getCollection() {
        return collection;
    }

    public String serialize() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().withRootName(root);
        String result = null;
        try {
            result = writer.writeValueAsString(collection);
        } catch (JsonProcessingException e) {
            result = e.getMessage();
        }
        return result;
    }
}

Et un exemple d'application :

public class Main {

    private static class UsersResource extends BaseCollectionsResponse<ArrayList<UserResource>> {
        public UsersResource() {
            super("users", new ArrayList<UserResource>());
        }
    }

    private static class UserResource extends BaseSingularResponse {

        private String name;
        private String id = UUID.randomUUID().toString();

        public UserResource(String userName) {
            super("user");
            this.name = userName;
        }

        public String getUserName() {
            return this.name;
        }

        public String getUserId() {
            return this.id;
        }
    }

    public static void main(String[] args) throws JsonProcessingException {
        UsersResource userCollection = new UsersResource();
        UserResource user1 = new UserResource("John");
        UserResource user2 = new UserResource("Jane");
        UserResource user3 = new UserResource("Martin");

        System.out.println(user1.serialize());

        userCollection.getCollection().add(user1);
        userCollection.getCollection().add(user2);
        userCollection.getCollection().add(user3);

        System.out.println(userCollection.serialize());
    }
}

Vous pouvez également utiliser l'annotation Jackson @JsonTypeInfo dans un niveau de classe

@JsonTypeInfo(include=As.WRAPPER_OBJECT, use=JsonTypeInfo.Id.NAME)
8
Arnaud Develay 3 févr. 2017 à 14:32

Je suppose que vous recherchez Sérialiseur Jackson personnalisé. Avec une implémentation de code simple, le même objet peut être sérialisé dans différentes structures

Un exemple: https://stackoverflow.com/a/10835504/814304 http://www.davismol.net/2015/05/18/jackson-create-and-register-a-custom-json-serializer-with-stdserializer-and-simplemodule-classes/

1
Community 23 mai 2017 à 12:32

Jackson n'a pas beaucoup de support pour les structures JSON dynamiques / variables, donc toute solution qui accomplit quelque chose comme ça va être assez piratée comme vous l'avez mentionné. Pour autant que je sache et d'après ce que j'ai vu, la méthode standard et la plus courante consiste à utiliser des classes wrapper comme vous l'êtes actuellement. Les classes wrapper s'additionnent, mais si vous faites preuve de créativité avec votre héritage, vous pourrez peut-être trouver des points communs entre les classes et ainsi réduire le nombre de classes wrapper. Sinon, vous pourriez envisager d'écrire un framework personnalisé.

1
Trevor Bye 1 févr. 2017 à 16:58

Personnellement, cela ne me dérange pas les classes Dto supplémentaires, vous n'avez besoin de les créer qu'une seule fois, et il y a peu ou pas de frais de maintenance. Et si vous devez effectuer des tests MockMVC, vous aurez probablement besoin des classes pour désérialiser vos réponses JSON afin de vérifier les résultats.

Comme vous le savez probablement, le framework Spring gère la sérialisation / désérialisation des objets dans la couche HttpMessageConverter, c'est donc le bon endroit pour modifier la façon dont les objets sont sérialisés.

Si vous n'avez pas besoin de désérialiser les réponses, il est possible de créer un wrapper générique et un HttpMessageConverter personnalisé (et placez-le avant MappingJackson2HttpMessageConverter dans la liste du convertisseur de messages). Comme ça:

public class JSONWrapper {

    public final String name;
    public final Object object;

    public JSONWrapper(String name, Object object) {
        this.name = name;
        this.object = object;
    }
}


public class JSONWrapperHttpMessageConverter extends MappingJackson2HttpMessageConverter {

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        // cast is safe because this is only called when supports return true.
        JSONWrapper wrapper = (JSONWrapper) object;
        Map<String, Object> map = new HashMap<>();
        map.put(wrapper.name, wrapper.object);
        super.writeInternal(map, type, outputMessage);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return clazz.equals(JSONWrapper.class);
    }
}

Vous devez ensuite enregistrer le HttpMessageConverter personnalisé dans la configuration de ressort qui étend WebMvcConfigurerAdapter en remplaçant configureMessageConverters(). Sachez que cela désactive la détection automatique par défaut des convertisseurs, vous devrez donc probablement ajouter la valeur par défaut vous-même (vérifiez le code source Spring pour WebMvcConfigurationSupport#addDefaultHttpMessageConverters() pour voir les valeurs par défaut. Si vous étendez WebMvcConfigurationSupport à la place {{ X4}} vous pouvez appeler addDefaultHttpMessageConverters directement (Personnellement, je préfère utiliser WebMvcConfigurationSupport plutôt que WebMvcConfigurerAdapter si j'ai besoin de personnaliser quoi que ce soit, mais il y a quelques implications mineures à faire cela, que vous pouvez probablement lire à propos dans d'autres articles.

4
Klaus Groenbaek 6 févr. 2017 à 14:18