web-dev-qa-db-fra.com

Spring Boot: insertion de la réponse JSON dans des objets parents dynamiques

J'ai une spécification d'API REST qui communique avec les microservices dorsaux, qui renvoient les valeurs suivantes:

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

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

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

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

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

Fondamentalement, je pense que c’est une approche acceptable pour minimiser les changements importants entre les mises à jour des API. Cependant, la traduction de ce modèle en code s’avère très ardue.

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

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 ne pouvons exposer que seulement les champs des modèles utilisés en interne pour lesquels nous avons des accesseurs publics, mais pas d'autres. Ensuite, pour les réponses aux 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 génère des réponses JSON qui sont formatées conformément aux spécifications 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. L'inconvénient majeur est que j'ai une tonne de classes d'emballage volantes qui n'effectuent aucune tâche logique perceptible, à part d'être lue par Jackson pour produire une réponse correctement formatée.

Mes questions sont les suivantes:

  • Comment puis-je éventuellement 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 devrais utiliser quelque chose comme Javaassist pour ajouter champs pour les classes de réponse de base à Runtime - un sale bidouillage que je voudrais rester aussi loin que possible.

  • Y a-t-il un moyen plus facile d'y parvenir? Malheureusement, il se peut que la réponse contienne un nombre variable d’objets JSON de niveau supérieur dans un an. Je ne peux donc pas utiliser un élément comme le SerializationConfig.Feature.WRAP_ROOT_VALUE de Jackson, car cela encapsule everything dans un seul objet de niveau racine (dans la mesure du possible). suis au courant).

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

13
user991710

Il y a 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)
6
Arnaud Develay

Personnellement, les classes Dto supplémentaires ne me dérangent pas, il suffit de les créer une fois et les coûts de maintenance sont minimes, voire nuls. 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, la structure Spring gère la sérialisation/désérialisation des objets dans la couche HttpMessageConverter, de sorte que vous pouvez 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 convertisseur HttpMessageConverter personnalisé (et de le placer avant MappingJackson2HttpMessageConverter dans la liste du convertisseur de message). 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 convertisseur 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 l’ajouter vous-même (vérifiez le code source de Spring pour WebMvcConfigurationSupport#addDefaultHttpMessageConverters() pour voir les valeurs par défaut. Si vous étendez WebMvcConfigurationSupport à la place WebMvcConfigurerAdapter, vous pouvez appeler directement addDefaultHttpMessageConverters utiliser WebMvcConfigurationSupport sur WebMvcConfigurerAdapter si j'ai besoin de personnaliser quoi que ce soit, mais cela a quelques implications mineures, que vous pouvez probablement lire dans d'autres articles.

4
Klaus Groenbaek

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

quelques exemples: https://stackoverflow.com/a/10835504/814304http://www.davismol.net/2015/05/18/jackson-create-and- enregistrer-un-custom-json-serializer-with-stdserializer-and-simplemodule-classes/

1
iMysak

Jackson ne prend pas beaucoup en charge les structures JSON dynamiques/variables. Par conséquent, toute solution permettant de réaliser une telle opération sera assez compliquée, comme vous l'avez mentionné. 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 telles que vous les utilisez actuellement. Les classes wrapper s'additionnent, mais si vous êtes créatif 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 voudrez peut-être écrire un framework personnalisé.

1
Trevor Bye