JAdventure - Kurs zur Softwareentwicklung
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

1.4.3 Serialisierung

Einfache Serialisierung mit Gson

Die Serialisierung mit Gson ist relatov einfach und gradlinig. So kann ein Level einfach serialisiert und deserialisiert werden:

Gson gson = new Gson();
Level level = ....; // Beliebige Initialisierung
String json = gson.toJson(level, Level.class); // Serialisierung
// ...

Level obj = gson.fromJson(json, Level.class); // Deserialisierung

Serialisierung von abgeleiteten Klassen

Problematisch wird dies aber, wenn ich nicht weiss, von welcher Klasse genau das JSON stammt. Wenn es also eine von Level abgeleitete Klasse geben sollte, dann würde man bei der Serialisierung nur Elemente von Level serialisieren und beim Deserialisieren würde man eine Instanz von Level bekommen.

Um dieses Problem zu lösen gibt es mehrere Ansätze. Da wir aber vermeiden sollen, dass sich jede Klasse bei Gson registrieren muss, wollen wir einen mehr generischen Ansatz:

Bei jeder Serialisierung wird der Name der Klasse mit serialisiert. Bei der Deserialisierung wird erst der Name der Klasse gelesen und dann das Json zum deserialisieren dieser Klasse verwendet.

Speichern des Namens der Klasse

Jeder Klasse, die gepeichert werden soll, soll auch den Namen der Klasse enthalten. Eine Möglichkeit ist dabei einfach ein Feld, das den Namen der Klasse enthält. Dies hätte auch den Vorteil, dass wir bei der Serialisierung keinen Adapter benötigen:

/**
 * An instance that can be saved through an Adapter and contains the name of the class as field.
 */
@EqualsAndHashCode
@JsonAdapter(SavedObjectAdapter.class)
public abstract class SavedObject {
    /**
     * Class name of the instance.
     */
    @Getter
    private final String type = getClass().getName();
}

Hinweise:

  • Hier wurde schon der Adapter mit angegeben, den wir im Anschluss noch erstellen.
  • Streng genommen könnte man auf die Instanzvariable type verzichten und dieses Element im Adapter ins Json schreiben.

Adapter für SavedObject

Um einen Adapter zu implementieren schreiben wir eine Klasse, die JsonSerializer und JsonDeserializer implementiert:

final public class SavedObjectAdapter implements JsonSerializer<SavedObject>, JsonDeserializer<SavedObject> {

Zur Implementation gehören dann die Methoden zum serialisieren und deserialisieren.

Serialisieren einer SavedObject instanz

Bei der genannten Veriante haben wir bereits gesagt: Die Instanz muss einfach als genau ihre Klasse serialisiert werden.

    /**
     * Serializes an SavedObject to JSON. Makes sure that the serialization is using the correct class.
     * @param object Object to serialize.
     * @param interfaceType Not used.
     * @param context Serialization context.
     * @return The JsonElement that holds the serialized object.
     */
    @Override
    public JsonElement serialize(final SavedObject object, final Type interfaceType,
                                 final JsonSerializationContext context) {
        if (object == null) return null;
        return context.serialize(object, object.getClass());
    }

Deserialisieren

Beim Deserialisieren müssen dann erst auslesen, welche Klasse genau serialisiert wurde um dann diese Klasse zu lesen.

    /**
     * Deserializes the SavedInstance from a json.
     * @param elem Json element to deserialize
     * @param interfaceType not used.
     * @param context Deserialization context.
     * @return The restored instance with correct class.
     * @throws JsonParseException Throws an JsonParseException if deserialization is not possible.
     */
    @Override
    public SavedObject deserialize(final JsonElement elem, final Type interfaceType,
                                   final JsonDeserializationContext context) throws JsonParseException {
        final JsonObject wrapper = (JsonObject) elem;
        final JsonElement typeName = getType(wrapper);
        final Type actualType = typeForName(typeName);
        SavedObject result =  context.deserialize(elem, actualType);
        return result;
    }

Es wird erst der Typ ausgelesen. Dazu wurde eine Hilfsmethode getType geschrieben:

    /**
     * Gets the type for a given name.
     * @param typeElem JsonElement with classname inside.
     * @return The requested class.
     * @throws JsonParseException Thrown if the class is not available / known.
     */
    private Type typeForName(final JsonElement typeElem) throws JsonParseException {
        try {
            return Class.forName(typeElem.getAsString());
        } catch (ClassNotFoundException e) {
            throw new JsonParseException(e);
        }
    }

Im Anschluss wird vom Element mit dem Type eine Klasse geholt:

    /**
     * Gets the type for a given name.
     * @param typeElem JsonElement with classname inside.
     * @return The requested class.
     * @throws JsonParseException Thrown if the class is not available / known.
     */
    private Type typeForName(final JsonElement typeElem) throws JsonParseException {
        try {
            return Class.forName(typeElem.getAsString());
        } catch (ClassNotFoundException e) {
            throw new JsonParseException(e);
        }
    }

Damit können wir nun beliebige Elemente Serialisieren, die von SavedObject erben.

Veränderungen an den Entities

Nun können wir GameObject und Level von SavedObject ableiten.

Zweites Problem: Zirkuläre Referenz

Ein weiteres Problem ist die zirkuläre Referenz: Level hat eine Liste von GameObjects, die im Level sind. Jedes GameObject hat in position aber auch eine Referenz zu dem Objekt, in dem es ist.

Damit die Serialisierung klappt, haben wir die parent Referent transient gemacht: Dadurch wird diese Referenz nicht mit serialisiert.

Dieses Problem müssen wir nun auch noch lösen. Eine Möglichkeit ist, dass wir Klassen, welche solche Child Objekte haben, erkennen können. Dann könnten wir bei der Deserialisierung darauf prüfen und ggf. die Children durch gehen.

Interface Container

Zur Erkennung solcher Klassen führen wir einfach ein Interface Container ein:

/**
 * A Container has a list of GameObject as children.
 */
public interface Container {
    /**
     * Gets the List of GameObject children.
     * @return List of GameObjects.
     */
    List<GameObject> getChildren();
}

Dieses Interface implementiert dann Level.

In der Deserialisierung von SavedObjects können wir dies nun noch abprüfen:

    /**
     * Deserializes the SavedInstance from a json.
     * @param elem Json element to deserialize
     * @param interfaceType not used.
     * @param context Deserialization context.
     * @return The restored instance with correct class.
     * @throws JsonParseException Throws an JsonParseException if deserialization is not possible.
     */
    @Override
    public SavedObject deserialize(final JsonElement elem, final Type interfaceType,
                                   final JsonDeserializationContext context) throws JsonParseException {
        final JsonObject wrapper = (JsonObject) elem;
        final JsonElement typeName = getType(wrapper);
        final Type actualType = typeForName(typeName);
        SavedObject result =  context.deserialize(elem, actualType);
        setParentReferencesIfRequired(result);
        return result;
    }

    /**
     * The parent reference inside the Position of a GameObject is transient and is not saved.
     * So when deserializing a container, we have to set the parent again.
     * @param object SavedObject to check and set Parent if required.
     */
    private void setParentReferencesIfRequired(final SavedObject object) {
        if (object instanceof Container container) {
            container.getChildren().stream()
                    .map(GameObject::getPosition)
                    .filter(Objects::nonNull)
                    .forEach(p -> p.setParent(container));
        }
    }

Bei der Deserialisierung wird eine Methode aufgerufen, die bei einem Container alle Elemente durchgeht und wenn diese eine Position haben, dann wird in der Position die Parent Instanz gesetzt.

Serialisierung von Listen mit GameObjects

Nun müssen wir natürlich noch sicher gehen, dass bei der Serialisierung von einer Liste mit SaveObjects diese auch korrekt gespeichert bzw. geladen wird. Dazu schreiben wir auch einen kleinen Adapter, der bei der Serialisierung jedes Element mit der eigentliche Klasse serialisiert und bei der Deserialisierung dann dei Klasse verwendet, die es ist.

/**
 * Gson Adapter to (de-)serialize a List of SavedObjects
 * Prefix the List with @JsonAdapter(ListOfSavedObjectAdapter.class) to use this Adapter
 */
final public class ListOfSavedObjectAdapter implements JsonSerializer<List<? extends SavedObject>>, JsonDeserializer<List<? extends SavedObject>> {
    /**
     * Serializes a list of SavedInstance to JSON.
     * @param list Object to serialize.
     * @param interfaceType Not used.
     * @param context Serialization context.
     * @return The JsonElement that holds the serialized list.
     */
    public JsonElement serialize(final List<? extends SavedObject> list, final Type interfaceType,
                                 final JsonSerializationContext context) {
        if (list == null) return null;
        final JsonArray array = new JsonArray();
        for (SavedObject obj : list) {
            array.add(context.serialize(obj, SavedObject.class));
        }
        return array;
    }

    /**
     * Deserializes the list of SavedInstance from a json.
     * @param elem Json element to deserialize
     * @param interfaceType not used.
     * @param context Deserialization context.
     * @return The restored list with elements with correct class.
     * @throws JsonParseException Throws an JsonParseException if deserialization is not possible.
     */
    public List<? extends SavedObject> deserialize(final JsonElement elem, final Type interfaceType,
                                         final JsonDeserializationContext context) throws JsonParseException {
        List<SavedObject> result = new ArrayList<>();
        final JsonArray array = (JsonArray) elem;
        for (JsonElement element : array.asList()) {
            result.add(context.deserialize(element, SavedObject.class));
        }
        return result;
    }
}