5 votes

Choisir dynamiquement la classe de l'objet à créer à partir d'un fichier json

J'ai un problème intéressant pour lequel j'ai du mal à trouver une solution propre. Mon application lit des collections d'objets json qu'elle doit désérialiser vers tel ou tel type de classe en fonction d'un champ dans le json lui-même. Je n'ai aucun contrôle sur la structure du json ou sur la façon dont il arrive dans mon application.

J'ai créé des modèles pour chaque type d'objet susceptible d'arriver dans l'application et j'en suis arrivé à un point où j'essaie de construire un service qui extrait le champ 'type' et utilise ObjectMapper pour désérialiser le json vers le modèle approprié.

Exemple de Json :

{
    "message_type" : "model1"
    "other data" : "other value"
    ...
}

Modèles :

public class Model1 {
    ...
}

public class Model2 {
    ...
}

Service ?

public class DynamicMappingService {

    public ???? mapJsonToObject(String json) {
        String type = pullTypeFromJson();

        ???
    }

    private String pullTypeFromJson() {...}
}

Je ne veux pas d'une déclaration de commutation massive qui dit "Si la valeur du type est ceci, alors désérialisez en cela", mais j'ai du mal à trouver quelque chose de propre qui fasse cela. J'ai pensé à une classe de modèle générique où le paramètre générique est le type de modèle et le seul champ est l'instance de ce type de modèle, mais cela ne semble pas correct non plus et je ne suis pas sûr de ce que cela m'apporte. Je pourrais également avoir une sorte de classe abstraite vide que tous les modèles étendent, mais cela semble également horrible. Comment faire ? Les points supplémentaires pour un exemple.

1voto

roookeee Points 610

Vous pouvez utiliser le modèle de visiteur ici :

class ScratchStackOverflowQuestion57102092 {
    interface Reserializer {
        void accept(Model1 model1);
        void accept(Model2 model2);
    }

    interface Reserializeable {
        void visit(Reserializer reserializer);
    }

    class Model1 implements Reserializeable {
        @Override
        public void visit(Reserializer reserializer) {
            reserializer.accept(this);
        }
    }

    class Model2 implements Reserializeable {
        @Override
        public void visit(Reserializer reserializer) {
            reserializer.accept(this);
        }
    }

    public class ReserializerImpl implements Reserializer {

        @Override
        public void accept(Model1 model1) {
            //TODO: reserialize and push the new object somewhere
        }

        @Override
        public void accept(Model2 model2) {
            //TODO: reserialize and push the new object somewhere
        }
    }

    public class JsonConversion {
        //TODO: instantiate etc
        private Reserializer reserializer;

        public void handleJson(String json) {
            //TODO: use some framework like Jackson which can read type hints from JSON fields
            Reserializeable serialized = mapFromJson(json);
            serialized.visit(reserializer);
        }

    }
}

Il s'agit d'un exemple simplifié de la manière d'accomplir ce que vous voulez, mais il manque les fonctionnalités suivantes pour l'instant :

  • il ne renvoie rien car il faudrait avoir un autre modèle de visiteur et implémenter un récepteur pour chaque objet re-sérialisé (comme vous l'avez appelé)
  • il faut encore implémenter / trouver une bibliothèque qui lit les indices de type à partir du json reçu (comme l'indique le commentaire du code, jackson peut le faire)

Il se peut donc que vous deviez adapter un peu le code donné :)

EDIT : A la demande générale, une implémentation complète qui permet la gestion dynamique des objets re-sérialisés via un autre visiteur (il manque seulement l'utilisation de Jackson qui prend en compte une indication de type dans le JSON). Je m'excuse pour la douzaine de classes, mais il n'y a pas de moyen plus court. Voir la fonction exampleUsage() sur la manière dont cette approche est utilisée / comment définir des gestionnaires pour les différents objets reconvertis :

class ScratchStackOverflowQuestion57102092_V2 {
//////////////////////////////// INPUTS //////////////////////////////
    interface Reserializer {
        void accept(Model1 model1);
        void accept(Model2 model2);
    }

    interface Reserializeable {
        void visit(Reserializer reserializer);
    }

    class Model1 implements Reserializeable {
        @Override
        public void visit(Reserializer reserializer) {
            reserializer.accept(this);
        }
    }

    class Model2 implements Reserializeable {
        @Override
        public void visit(Reserializer reserializer) {
            reserializer.accept(this);
        }
    }

//////////////////////////////// RECONVERSION /////////////////////////

    interface ReconvertedVisitor {
        void accept(ReconvertedModel1 reconverted);
        void accept(ReconvertedModel2 reconverted);
    }

    interface ReconvertedModel {
        void visit(ReconvertedVisitor visitor);
    }

    //Some dummy object as an example
    class ReconvertedModel1 implements ReconvertedModel{

        @Override
        public void visit(ReconvertedVisitor visitor) {
            visitor.accept(this);
        }
    }

    //Some dummy object as an example
    class ReconvertedModel2 implements ReconvertedModel{
        @Override
        public void visit(ReconvertedVisitor visitor) {
            visitor.accept(this);
        }
    }

////////////////////////////// IMPLEMENTATIONS ///////////////////////////////
    public class ReserializerImpl implements Reserializer {

        private final ReconvertedVisitor visitor;

        public ReserializerImpl(ReconvertedVisitor visitor) {
            this.visitor = visitor;
        }

        @Override
        public void accept(Model1 model1) {
            //TODO: do some real conversion
            ReconvertedModel1 reserializeResult = new ReconvertedModel1();
        }

        @Override
        public void accept(Model2 model2) {
            //TODO: do some real conversion
            ReconvertedModel2 reserializeResult = new ReconvertedModel2();
        }
    }

    public class JsonConversion {

        public void handleJson(String json, ReconvertedVisitor handler) {
            //TODO: use some framework like Jackson which can read type hints from JSON fields
            Reserializeable serialized = mapFromJson(json);
            ReserializerImpl reserializer = new ReserializerImpl(handler);
            serialized.visit(reserializer);
        }
    }

    public void exampleUsage() {
        //Just some sample, you could delegate to different objects in each accept
        class PrintingReconvertedVisitor implements ReconvertedVisitor {

            @Override
            public void accept(ReconvertedModel1 reconverted) {
                System.out.println(reconverted);
            }

            @Override
            public void accept(ReconvertedModel2 reconverted) {
                System.out.println(reconverted);
            }
        }
        JsonConversion conversion = new JsonConversion();
        conversion.handleJson("TODO: SOME REAL JSON HERE", new PrintingReconvertedVisitor());
    }
}

Je ne suis pas très satisfait de l'appellation des classes. Reserializer donc ModelVisitor ou quelque chose d'approprié.

1voto

daniu Points 8648

Vous avez deux problèmes différents ici : créer un objet de type inconnu et faire quelque chose de significatif avec lui par la suite. Si le type créé dépend vraiment de la valeur d'un noeud dans le json, vous n'aurez pas le temps de créer une correspondance entre ce String et la classe qu'il créera. Un énorme bloc if-then-else ne sera pas utile au-delà de deux ou trois classes.

Création

Vous pouvez créer un singleton de registre que vous utiliserez pour maintenir cette correspondance.

public enum ClassByNodeMapping {
    INSTANCE;
    final Map<String, Class<?>> mapping = new HashMap<>();

    public void addMapping(String nodeValue, Class<?> clazz) {
        mapping.put(nodeValue, clazz);
    }
    public Class<?> getMapping(String nodeValue) {
        return mapping.get(nodeValue);
    }
}

Vous pouvez remplir ce formulaire par classe :

@Data
class Model1 {
    static {
        ClassByNodeMapping.INSTANCE.addMapping("model1", Model1.class);
    }
    private String model1Value;
}

Mais même dans ce cas, vous devrez instancier Model1 une fois avant d'être enregistré (pour garantir que la classe a été chargée). Votre code client ressemblerait donc à ceci :

class Scratch {
    static {
        // need to instantiate the models once to trigger registration
        Model1 model = new Model1();
        Model2 model2 = new Model2();
    }
    private static final ObjectMapper mapper = new ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    public static void main(String[] args) throws IOException {
        String json1 = "{\"type\": \"model1\", \"model1Value\": \"m1\"}";
        String json2 = "{\"type\": \"model2\", \"model2Value\": \"m2\"}";

        System.out.println(deserialize(json1));
        System.out.println(deserialize(json2));
    }

    private static Object deserialize(String json) throws IOException {
        JsonNode jsonNode = mapper.readTree(json);
        Class<?> type = ClassByNodeMapping.INSTANCE.getMapping(jsonNode.get("type").textValue());
        return mapper.readValue(json, type);
    }
}

Traitement

Vous avez maintenant créé des objets, mais comme ils sont de type Object Il n'y a pas grand-chose à en faire. Dans les commentaires à votre question, vous avez dit quelque chose comme "reserialize", je ne suis pas sûr de ce que cela signifie ; le concept général est la consommation ici - l'objet est créé et ensuite quelque chose est fait avec lui. Vous pouvez utiliser le concept de visiteur de l'autre réponse ici, mais je le trouve quelque peu confus.

Vous pouvez étendre le mappage en incluant également un gestionnaire. En général, il est préférable de séparer les deux, mais dans ce cas, cela peut s'avérer nécessaire pour préserver la sécurité des types.

enum ClassByNodeMapping {
    INSTANCE;
    final Map<String, Class<?>> mapping = new HashMap<>();
    final Map<Class<?>, Consumer<?>> handlerMapping = new HashMap<>();

    public <T>void addMapping(String nodeValue, Class<T> clazz, Consumer<T> handler) {
        mapping.put(nodeValue, clazz);
        handlerMapping.put(clazz, handler);
    }
    public Class<?> getMapping(String nodeValue) {
        return mapping.get(nodeValue);
    }
    public Consumer<Object> getHandler(Class<?> clazz) {
        return (Consumer<Object>) handlerMapping.get(clazz);
    }
}

Votre code d'enregistrement ressemble donc à ceci

@Data
class Model1 {
    static {
        // you'd probably not want to do the registration here,
        // assuming your handler code is outside
        ClassByNodeMapping.INSTANCE.addMapping("model1", Model1.class, Model1::handle);
    }
    private String model1Value;

    private static void handle(Model1 m1) {
        System.out.printf("handling as model1: %s%n", m1);
    }
}

Et votre code client (test)

public static void main(String[] args) throws IOException {
    String json1 = "{\"type\": \"model1\", \"model1Value\": \"m1\"}";
    String json2 = "{\"type\": \"model2\", \"model2Value\": \"m2\"}";

    handle(json1);
    handle(json2);
}

private static void handle(String json) throws IOException {
    JsonNode jsonNode = mapper.readTree(json);
    Class<?> type = ClassByNodeMapping.INSTANCE.getMapping(jsonNode.get("type").textValue());
    Consumer<Object> handler = ClassByNodeMapping.INSTANCE.getHandler(type);
    Object o = mapper.readValue(json, type);
    handler.accept(o);
}

Mise en garde

Je considérerais le concept dans son ensemble comme une odeur de code. Je m'efforcerais certainement de faire en sorte que le canal entrant ne contienne que des données d'une valeur de 1,5 million d'euros.

0voto

ACV Points 5246

Vous avez besoin Méthode de l'usine ou / et Usine abstraite pour cela.

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