web-dev-qa-db-de.com

Spring Boot: Umschließen der JSON-Antwort in dynamische übergeordnete Objekte

Ich habe eine REST -API-Spezifikation, die mit Back-End-Mikrodienstleistungen spricht, die die folgenden Werte zurückgeben:

Bei "Sammlungen" -Antworten (z. B. GET/Benutzer):

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

Bei "Einzelobjekt" -Antworten (z. B. GET /users/{userUuid}):

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

Dieser Ansatz wurde so gewählt, dass einzelne Antworten erweiterbar sind (wenn beispielsweise GET /users/{userUuid} einen zusätzlichen Abfrageparameter in der Zeile erhält, wie beispielsweise bei ?detailedView=true, hätten wir zusätzliche Anforderungsinformationen).

Grundsätzlich denke ich, dass dies ein OK-Ansatz ist, um grundlegende Änderungen zwischen API-Aktualisierungen zu minimieren. Die Umsetzung dieses Modells in Code erweist sich jedoch als sehr mühsam.

Angenommen, für Einzelantworten habe ich das folgende API-Modellobjekt für einen einzelnen Benutzer:

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
}

Der Vorteil dieser Methode ist, dass wir nur die Felder der intern verwendeten Modelle, für die wir öffentliche Getter haben, verfügbar machen können, andere jedoch nicht. Dann hätte ich für Sammlungen Antworten die folgende Wrapper-Klasse:

public class UsersResource extends ResourceSupport {

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

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

Für Einzelobjekt-Antworten hätten wir Folgendes:

public class UserResource {

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

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

Dies ergibt JSON-Antworten, die gemäß der API-Spezifikation oben in diesem Beitrag formatiert sind. Der Vorteil dieses Ansatzes ist, dass wir nur diejenigen Felder verfügbar machen, die wir (wollen} _ freigeben möchten. Der große Nachteil ist, dass ich eine Tonne von Wrapper-Klassen hat, die herumfliegen und keine erkennbaren logischen Aufgaben ausführen, außer dass sie von Jackson gelesen werden, um eine korrekt formatierte Antwort zu erhalten.

Meine Fragen lauten wie folgt:

  • Wie kann ich diesen Ansatz verallgemeinern? Im Idealfall hätte ich gerne eine einzige BaseSingularResponse-Klasse (und möglicherweise eine BaseCollectionsResponse extends ResourceSupport-Klasse), die alle meine Modelle erweitern können. Aber da Jackson die JSON-Schlüssel aus den Objektdefinitionen abzuleiten scheint, müsste ich etwas wie Javaassist hinzufügen Felder zu den Basisantwortklassen bei Runtime - ein schmutziger Hack, den ich gerne so weit wie möglich von Menschen fernhalten möchte.

  • Gibt es einen einfacheren Weg, dies zu erreichen? Leider habe ich in der Antwort in einem Jahr möglicherweise eine variable Anzahl von Top-Level-JSON-Objekten. Daher kann ich nicht etwas wie Jacksons SerializationConfig.Feature.WRAP_ROOT_VALUE verwenden, da dies alles in ein einziges Objekt auf Root-Ebene (soweit) umschließt wie mir bekannt ist).

  • Gibt es vielleicht etwas wie @JsonProperty für Klassenebene (im Gegensatz zu nur Methoden- und Feldebene)?

13
user991710

Es gibt mehrere Möglichkeiten.

Sie können einen Java.util.Map verwenden:

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));

Sie können ObjectWriter verwenden, um die Antwort, die Sie wie folgt verwenden können, einzuhüllen:

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

Hier ist ein Vorschlag zur Verallgemeinerung dieser Serialisierung.

Eine Klasse zur Behandlung einfacher Objekte :

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;
    }
}

Eine Klasse für die Sammlung :

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;
    }
}

Und eine Beispielanwendung :

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());
    }
}

Sie können die Jackson-Anmerkung @JsonTypeInfo auch auf Klassenebene verwenden

@JsonTypeInfo(include=As.WRAPPER_OBJECT, use=JsonTypeInfo.Id.NAME)
6
Arnaud Develay

Ich persönlich kümmere mich nicht um die zusätzlichen Dto-Klassen, Sie müssen sie nur einmal erstellen, und die Wartungskosten sind gering. Und wenn Sie MockMVC-Tests durchführen müssen, benötigen Sie höchstwahrscheinlich die Klassen, um Ihre JSON-Antworten zu deserialisieren, um die Ergebnisse zu überprüfen.

Wie Sie wahrscheinlich wissen, übernimmt das Spring-Framework die Serialisierung/Deserialisierung von Objekten im HttpMessageConverter-Layer, sodass Sie die Art und Weise, wie Objekte serialisiert werden, an der richtigen Stelle ändern können.

Wenn Sie die Antworten nicht deserialisieren müssen, können Sie einen generischen Wrapper und einen benutzerdefinierten HttpMessageConverter erstellen (und ihn vor MappingJackson2HttpMessageConverter in die Liste der Nachrichtenkonverter einfügen). So was:

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);
    }
}

Sie müssen dann den benutzerdefinierten HttpMessageConverter in der Federkonfiguration registrieren, die WebMvcConfigurerAdapter verlängert, indem Sie configureMessageConverters() überschreiben. Beachten Sie, dass dadurch die standardmäßige automatische Erkennung von Konvertern deaktiviert wird. Sie müssen also wahrscheinlich den Standard selbst hinzufügen (überprüfen Sie den Spring-Quellcode für WebMvcConfigurationSupport#addDefaultHttpMessageConverters(), um die Standardeinstellungen anzuzeigen. Wenn Sie WebMvcConfigurationSupport statt WebMvcConfigurerAdapter Sie erweitern kann addDefaultHttpMessageConverters direkt anrufen (Persönlich bevorzuge ich WebMvcConfigurationSupport over WebMvcConfigurerAdapter, wenn ich etwas anpassen muss, aber es gibt einige geringfügige Auswirkungen auf diese Vorgehensweise, die Sie wahrscheinlich in anderen Artikeln nachlesen können .

4
Klaus Groenbaek

Ich denke, Sie suchen Custom Jackson Serializer . Bei einer einfachen Codeimplementierung kann dasselbe Objekt in verschiedenen Strukturen serialisiert werden

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

1
iMysak

Jackson hat nicht viel Unterstützung für dynamische/variable JSON-Strukturen. Daher wird jede Lösung, die so etwas bewirkt, ziemlich hackig sein, wie Sie bereits erwähnt haben. Soweit ich weiß und von dem, was ich gesehen habe, ist die gängigste Standardmethode die Verwendung von Wrapper-Klassen, wie Sie derzeit verwendet werden. Die Wrapper-Klassen summieren sich zwar, aber wenn Sie mit Ihrer Vererbung kreativ werden, können Sie möglicherweise einige Gemeinsamkeiten zwischen Klassen finden und so die Anzahl der Wrapper-Klassen reduzieren. Andernfalls schreiben Sie möglicherweise ein benutzerdefiniertes Framework.

1
Trevor Bye