it-swarm.com.de

Polymorphismus mit Gson

Ich habe ein Problem beim Deserialisieren eines Json-Strings mit Gson. Ich erhalte eine Reihe von Befehlen. Der Befehl kann gestartet, gestoppt oder eine andere Art von Befehl sein. Natürlich habe ich Polymorphismus und Start/Stopp-Befehl von Befehl erben.

Wie kann ich es mit gson zurück zum richtigen Befehlsobjekt serialisieren?

Es scheint, dass ich nur den Basistyp erhalte, also den deklarierten Typ und niemals den Laufzeit-Typ.

93
Sophie

Das ist ein bisschen spät, aber ich musste heute genau dasselbe tun. Basierend auf meinen Recherchen und unter Verwendung von gson-2.0 möchten Sie also nicht die registerTypeHierarchyAdapter -Methode verwenden, sondern die einfachere registerTypeAdapter . Und Sie müssen auf keinen Fall instanceofs ausführen oder Adapter für die abgeleiteten Klassen schreiben: nur einen Adapter für die Basisklasse oder das Interface, vorausgesetzt natürlich, Sie sind mit dem Standard zufrieden Serialisierung der abgeleiteten Klassen. Wie auch immer, hier ist der Code (Paket und Importe entfernt) (auch verfügbar in github ):

Die Basisklasse (Schnittstelle in meinem Fall):

public interface IAnimal { public String sound(); }

Die beiden abgeleiteten Klassen Cat:

public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}

Und Hund:

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}

Der IAnimalAdapter:

public class IAnimalAdapter implements JsonSerializer<IAnimal>, JsonDeserializer<IAnimal>{

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(IAnimal src, Type typeOfSrc,
            JsonSerializationContext context) {

        JsonObject retValue = new JsonObject();
        String className = src.getClass().getName();
        retValue.addProperty(CLASSNAME, className);
        JsonElement elem = context.serialize(src); 
        retValue.add(INSTANCE, elem);
        return retValue;
    }

    @Override
    public IAnimal deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException  {
        JsonObject jsonObject = json.getAsJsonObject();
        JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
        String className = prim.getAsString();

        Class<?> klass = null;
        try {
            klass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

Und die Testklasse:

public class Test {

    public static void main(String[] args) {
        IAnimal animals[] = new IAnimal[]{new Cat("Kitty"), new Dog("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(IAnimal.class, new IAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, IAnimal.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            IAnimal animal2 = gsonExt.fromJson(animalJson, IAnimal.class);
            System.out.println(animal2.sound());
        }
    }
}

Wenn Sie den Test :: main ausführen, erhalten Sie die folgende Ausgabe:

serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Cat","INSTANCE":{"name":"Kitty"}}
Kitty : "meaow"
serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Dog","INSTANCE":{"name":"Brutus","ferocity":5}}
Brutus : "bark" (ferocity level:5)

Ich habe das oben Gesagte tatsächlich mit der registerTypeHierarchyAdapter -Methode durchgeführt, aber dafür waren anscheinend benutzerdefinierte DogAdapter- und CatAdapter-Serializer-/Deserializer-Klassen erforderlich Füge ein weiteres Feld zu Hund oder Katze hinzu.

114

Gson verfügt derzeit über einen Mechanismus zum Registrieren eines Typhierarchie-Adapters , der Berichten zufolge für die einfache polymorphe Deserialisierung konfiguriert werden kann, aber ich verstehe nicht, wie dies der Fall ist, da ein Typhierarchie-Adapter nur eine Kombination zu sein scheint Serializer/Deserializer/Instanzersteller, wobei die Details der Instanzerstellung dem Codierer überlassen bleiben, ohne dass eine tatsächliche polymorphe Typregistrierung erfolgt.

Es sieht so aus, als ob Gson bald die RuntimeTypeAdapter für eine einfachere polymorphe Deserialisierung haben wird. Weitere Informationen finden Sie unter http://code.google.com/p/google-gson/issues/detail?id=231 .

Wenn die Verwendung des neuen RuntimeTypeAdapter nicht möglich ist und Sie Gson verwenden müssen, müssen Sie Ihrer Meinung nach eine eigene Lösung erstellen und einen benutzerdefinierten Deserializer entweder als Typhierarchie-Adapter oder als Typadapter registrieren. Das Folgende ist ein solches Beispiel.

// output:
//     Starting machine1
//     Stopping machine2

import Java.lang.reflect.Type;
import Java.util.HashMap;
import Java.util.Map;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;

public class Foo
{
  // [{"machine_name":"machine1","command":"start"},{"machine_name":"machine2","command":"stop"}]
  static String jsonInput = "[{\"machine_name\":\"machine1\",\"command\":\"start\"},{\"machine_name\":\"machine2\",\"command\":\"stop\"}]";

  public static void main(String[] args)
  {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    CommandDeserializer deserializer = new CommandDeserializer("command");
    deserializer.registerCommand("start", Start.class);
    deserializer.registerCommand("stop", Stop.class);
    gsonBuilder.registerTypeAdapter(Command.class, deserializer);
    Gson gson = gsonBuilder.create();
    Command[] commands = gson.fromJson(jsonInput, Command[].class);
    for (Command command : commands)
    {
      command.execute();
    }
  }
}

class CommandDeserializer implements JsonDeserializer<Command>
{
  String commandElementName;
  Gson gson;
  Map<String, Class<? extends Command>> commandRegistry;

  CommandDeserializer(String commandElementName)
  {
    this.commandElementName = commandElementName;
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    gson = gsonBuilder.create();
    commandRegistry = new HashMap<String, Class<? extends Command>>();
  }

  void registerCommand(String command, Class<? extends Command> commandInstanceClass)
  {
    commandRegistry.put(command, commandInstanceClass);
  }

  @Override
  public Command deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException
  {
    try
    {
      JsonObject commandObject = json.getAsJsonObject();
      JsonElement commandTypeElement = commandObject.get(commandElementName);
      Class<? extends Command> commandInstanceClass = commandRegistry.get(commandTypeElement.getAsString());
      Command command = gson.fromJson(json, commandInstanceClass);
      return command;
    }
    catch (Exception e)
    {
      throw new RuntimeException(e);
    }
  }
}

abstract class Command
{
  String machineName;

  Command(String machineName)
  {
    this.machineName = machineName;
  }

  abstract void execute();
}

class Stop extends Command
{
  Stop(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Stopping " + machineName);
  }
}

class Start extends Command
{
  Start(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Starting " + machineName);
  }
}
12

Marcus Junius Brutus hatte eine großartige Antwort (danke!). Um sein Beispiel zu erweitern, können Sie seine Adapterklasse generisch für alle Arten von Objekten (nicht nur IAnimal) mit den folgenden Änderungen festlegen:

class InheritanceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T>
{
....
    public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)
....
    public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
....
}

Und in der Testklasse:

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}
8
user2242263

GSON hat hier einen ziemlich guten Testfall, der zeigt, wie ein Typhierarchieadapter definiert und registriert wird.

http://code.google.com/p/google-gson/source/browse/trunk/gson/src/test/Java/com/google/gson/functional/TypeHierarchyAdapterTest.java?r=739

Um das zu benutzen, mache folgendes:

    gson = new GsonBuilder()
          .registerTypeAdapter(BaseQuestion.class, new BaseQuestionAdaptor())
          .create();

Die Serialisierungsmethode des Adapters kann eine kaskadierende if-else-Überprüfung des Serialisierungstyps sein.

    JsonElement result = new JsonObject();

    if (src instanceof SliderQuestion) {
        result = context.serialize(src, SliderQuestion.class);
    }
    else if (src instanceof TextQuestion) {
        result = context.serialize(src, TextQuestion.class);
    }
    else if (src instanceof ChoiceQuestion) {
        result = context.serialize(src, ChoiceQuestion.class);
    }

    return result;

Deserialisierung ist ein bisschen hacky. Im Unit-Test-Beispiel wird geprüft, ob verräterische Attribute vorhanden sind, um zu entscheiden, für welche Klasse die Deserialisierung durchgeführt werden soll. Wenn Sie die Quelle des Objekts ändern können, das Sie serialisieren, können Sie jeder Instanz ein Attribut 'classType' hinzufügen, das die FQN des Namens der Instanzklasse enthält. Dies ist jedoch sehr unobjektorientiert.

6
k.c. sham

Google hat sein eigenes RuntimeTypeAdapterFactory veröffentlicht, um den Polymorphismus zu behandeln, aber leider ist es nicht Teil des gson-Kerns (Sie müssen die Klasse in Ihr Projekt kopieren und einfügen).

Beispiel:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

Hier Ich habe ein voll funktionsfähiges Beispiel mit den Modellen Tier, Hund und Katze veröffentlicht.

Ich denke, es ist besser, sich auf diesen Adapter zu verlassen, als ihn von Grund auf neu zu implementieren.

3
db80

Lange Zeit ist vergangen, aber ich konnte online keine wirklich gute Lösung finden.

Behalten Sie den gleichen Deserializer bei, aber entfernen Sie den Serializer -

public class IAnimalAdapter implements JsonDeSerializer<IAnimal> {
  private static final String CLASSNAME = "CLASSNAME";
  private static final String INSTANCE  = "INSTANCE";

  @Override
  public IAnimal deserialize(JsonElement json, Type typeOfT,
        JsonDeserializationContext context) throws JsonParseException  {
    JsonObject jsonObject =  json.getAsJsonObject();
    JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
    String className = prim.getAsString();

    Class<?> klass = null;
    try {
        klass = Class.forName(className);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        throw new JsonParseException(e.getMessage());
    }
    return context.deserialize(jsonObject.get(INSTANCE), klass);
  }
}

Fügen Sie dann in Ihrer ursprünglichen Klasse ein Feld mit @SerializedName("CLASSNAME") hinzu. Der Trick besteht nun darin, dies im Konstruktor der Basisklasse zu initialisieren, also machen Sie Ihre Schnittstelle zu einer abstrakten Klasse.

public abstract class IAnimal {
  @SerializedName("CLASSNAME")
  public String className;

  public IAnimal(...) {
    ...
    className = this.getClass().getName();
  }
}

Der Grund, warum es hier keine unendliche Rekursion gibt, ist, dass wir die tatsächliche Laufzeitklasse (d. H. Dog nicht IAnimal) an context.deserialize Übergeben. Dies ruft unseren Typadapter nicht auf, solange wir registerTypeAdapter und nicht registerTypeHierarchyAdapter verwenden.

2
Ginandi

Wenn Sie einen TypeAdapter für einen Typ und einen anderen für seinen Untertyp verwalten möchten, können Sie eine TypeAdapterFactory wie folgt verwenden:

public class InheritanceTypeAdapterFactory implements TypeAdapterFactory {

    private Map<Class<?>, TypeAdapter<?>> adapters = new LinkedHashMap<>();

    {
        adapters.put(Animal.class, new AnimalTypeAdapter());
        adapters.put(Dog.class, new DogTypeAdapter());
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        TypeAdapter<T> typeAdapter = null;
        Class<?> currentType = Object.class;
        for (Class<?> type : adapters.keySet()) {
            if (type.isAssignableFrom(typeToken.getRawType())) {
                if (currentType.isAssignableFrom(type)) {
                    currentType = type;
                    typeAdapter = (TypeAdapter<T>)adapters.get(type);
                }
            }
        }
        return typeAdapter;
    }
}

Diese Fabrik sendet den genauesten TypeAdapter

1
r3n0j

Aktualisierte Antwort - Die besten Teile aller anderen Antworten

Ich beschreibe Lösungen für verschiedene Anwendungsfälle und würde auch das Problem unendliche Rekursion ansprechen

  • Fall 1: Sie haben die Kontrolle über die Klassen , dh Sie können Ihr eigenes Cat, Dog schreiben Klassen sowie die IAnimal Schnittstelle. Folgen Sie einfach der Lösung von @ marcus-junius-brutus (die bestbewertete Antwort)

    Es wird keine unendliche Rekursion geben, wenn es eine gemeinsame Basisschnittstelle wie IAnimal gibt.

    Aber was ist, wenn ich das IAnimal oder eine solche Schnittstelle nicht implementieren möchte?

    Dann erzeugt @ marcus-junius-brutus (die bestbewertete Antwort) einen unendlichen Rekursionsfehler. In diesem Fall können wir wie folgt vorgehen.

    Wir müssten einen Kopierkonstruktor in der Basisklasse und eine Wrapper-Unterklasse wie folgt erstellen:

.

// Base class(modified)
public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }
    // COPY CONSTRUCTOR
    public Cat(Cat cat) {
        this.name = cat.name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}



    // The wrapper subclass for serialization
public class CatWrapper extends Cat{


    public CatWrapper(String name) {
        super(name);
    }

    public CatWrapper(Cat cat) {
        super(cat);
    }
}

Und der Serializer für den Typ Cat:

public class CatSerializer implements JsonSerializer<Cat> {

    @Override
    public JsonElement serialize(Cat src, Type typeOfSrc, JsonSerializationContext context) {

        // Essentially the same as the type Cat
        JsonElement catWrapped = context.serialize(new CatWrapper(src));

        // Here, we can customize the generated JSON from the wrapper as we want.
        // We can add a field, remove a field, etc.


        return modifyJSON(catWrapped);
    }

    private JsonElement modifyJSON(JsonElement base){
        // TODO: Modify something
        return base;
    }
}

Warum also ein Kopierkonstruktor?

Nun, sobald Sie den Kopierkonstruktor definiert haben, behält Ihr Wrapper dieselbe Rolle bei, unabhängig davon, wie stark sich die Basisklasse ändert. Zweitens müssten wir, wenn wir keinen Kopierkonstruktor definieren und die Basisklasse einfach in Unterklassen unterteilen, in Bezug auf die erweiterte Klasse "sprechen", d. H. CatWrapper. Es ist durchaus möglich, dass Ihre Komponenten in Bezug auf die Basisklasse und nicht auf den Wrapper-Typ sprechen.

Gibt es eine einfache Alternative?

Sicher, es wurde jetzt von Google eingeführt - dies ist die RuntimeTypeAdapterFactory -Implementierung:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

Hier müssten Sie ein Feld mit dem Namen "type" in Animal einfügen und den Wert desselben in Dog eingeben, um "dog" zu sein, Cat, um "cat" zu sein

Vollständiges Beispiel: https://static.javadoc.io/org.danilopianini/gson-extras/0.2.1/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.html

  • Fall 2: Sie haben keine Kontrolle über die Klassen . Sie treten einer Firma bei oder verwenden eine Bibliothek, in der die Klassen bereits definiert sind und Ihr Manager nicht möchte, dass Sie sie in irgendeiner Weise ändern. Sie können Ihre Klassen in Unterklassen unterteilen und sie eine gemeinsame Markierungsschnittstelle implementieren lassen (die keine Methoden hat ) wie AnimalInterface.

    Ex:

.

// The class we are NOT allowed to modify

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}


// The marker interface

public interface AnimalInterface {
}

// The subclass for serialization

public class DogWrapper  extends Dog implements AnimalInterface{

    public DogWrapper(String name, int ferocity) {
        super(name, ferocity);
    }

}

// The subclass for serialization

public class CatWrapper extends Cat implements AnimalInterface{


    public CatWrapper(String name) {
        super(name);
    }
}

Wir würden also CatWrapper anstelle von Cat, DogWrapper anstelle von Dog und AlternativeAnimalAdapter anstelle von IAnimalAdapter verwenden

// The only difference between `IAnimalAdapter` and `AlternativeAnimalAdapter` is that of the interface, i.e, `AnimalInterface` instead of `IAnimal`

public class AlternativeAnimalAdapter implements JsonSerializer<AnimalInterface>, JsonDeserializer<AnimalInterface> {

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(AnimalInterface src, Type typeOfSrc,
                                 JsonSerializationContext context) {

        JsonObject retValue = new JsonObject();
        String className = src.getClass().getName();
        retValue.addProperty(CLASSNAME, className);
        JsonElement elem = context.serialize(src); 
        retValue.add(INSTANCE, elem);
        return retValue;
    }

    @Override
    public AnimalInterface deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException  {
        JsonObject jsonObject = json.getAsJsonObject();
        JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
        String className = prim.getAsString();

        Class<?> klass = null;
        try {
            klass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

Wir führen einen Test durch:

public class Test {

    public static void main(String[] args) {

        // Note that we are using the extended classes instead of the base ones
        IAnimal animals[] = new IAnimal[]{new CatWrapper("Kitty"), new DogWrapper("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(AnimalInterface.class, new AlternativeAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, AnimalInterface.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            AnimalInterface animal2 = gsonExt.fromJson(animalJson, AnimalInterface.class);
        }
    }
}

Ausgabe:

serialized with the custom serializer:{"CLASSNAME":"com.examples_so.CatWrapper","INSTANCE":{"name":"Kitty"}}
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.DogWrapper","INSTANCE":{"name":"Brutus","ferocity":5}}
0
pulp_fiction