CodeGym /Kurse /Spring Boot /JSON-Konfiguration eines Typs: @JacksonComponent

JSON-Konfiguration eines Typs: @JacksonComponent

Spring Boot
Level 12, Lektion 2
Verfügbar

1. Gezielte Konfiguration vs spring.jackson.*

Ehrlich gesagt, nachdem man die Eigenschaften von spring.jackson.* kennengelernt hat, möchte man alles damit lösen. Es ist, als ob man entdeckt, dass es in der IDE Ctrl+Shift+F (Formatierung) gibt, und beginnt, es sogar auf den Glückwunschtext für die Oma anzuwenden. Aber globale Einstellungen „schießen“ per Definition auf die gesamte Anwendung und werden daher manchmal zu einem zu groben Werkzeug.

Stellen Sie sich vor, Sie möchten JSON nur für einen kleinen Value-Typ ändern. Nehmen wir als Beispiel Money – einen bekannten Typ aus der Katalog-Domäne. Hier ist es wichtig, zwei verschiedene Aufgaben nicht zu verwechseln: den Mechanismus der gezielten Konfiguration zu zeigen und die öffentliche Antwortform von CourseCard zu wählen. Uns interessiert jetzt genau der Mechanismus; die Form von CourseCard.price ist eine separate technische Entscheidung.

Hier ist ein typisches Symptom: Sie haben application.yaml geöffnet, ein paar Einstellungen hinzugefügt, den Dienst neu gestartet, das JSON angesehen… und plötzlich wurde nicht nur das Feld verbessert, für das Sie sich Mühe gegeben haben, sondern auch etwas mit Datumsangaben, Enums oder null-Feldern. In diesem Moment möchte man so tun, als wäre man es nicht gewesen, sondern „Boot selbst“. Aber wir sind erwachsene Leute: Wenn sich etwas geändert hat, dann haben wir es geändert.

Daher ist eine „gezielte“ Konfiguration erforderlich: ein Mechanismus, der Jackson etwa Folgendes sagt: „Wenn du auf den Typ Money triffst, verwende diese Logik. Alle anderen Typen lass in Ruhe.“ Genau dafür hat Jackson benutzerdefinierte Serialisierer/Deserialisierer, und Spring Boot 4 bietet eine sehr bequeme Möglichkeit, sie ohne manuelle Arbeit zu registrieren – @JacksonComponent.

Das heißt, uns interessiert jetzt nicht die neue kanonische Form des Preises, sondern die Möglichkeit, eine Regel an einen einzelnen Typ zu hängen.

2. Wo der Serialisierer lebt

Wenn wir über „benutzerdefinierten Serialisierer“ sprechen, stellt man sich leicht etwas im Controller vor: „Wir schreiben jetzt jsonMapper.writeValueAsString(...) direkt in der Methode und alles wird gut.“ Das ist wie ein Auto zu reparieren, indem man Schrauben durch das Autoradio festzieht: Theoretisch tut man etwas, aber es sieht seltsam aus und riecht nach architektonischen Problemen.

Das richtige mentale Modell ist: Der Controller gibt immer noch ein Objekt zurück, Spring MVC wählt einen geeigneten Message Converter, und dann, wenn Jackson die Felder des Objekts „durchläuft“, fragt er sich: „Ich habe jetzt den Typ Money. Wie soll ich ihn in JSON schreiben?“ Wenn es einen benutzerdefinierten Serialisierer gibt, wird er genau an dieser Stelle verwendet.

Lassen Sie uns dies als kleines Diagramm darstellen. Es erhebt keinen Anspruch auf vollständige dokumentarische Genauigkeit, zeigt aber gut den Platz von @JacksonComponent:

flowchart TD
    A["@RestController gibt CourseCard zurück"] --> B["Spring MVC HttpMessageConverter"]
    B --> C["Jackson 3 JsonMapper"]
    C --> D{"Typ des Feldes?"}
    D -->|String, int, LocalDate...| E["Standard-Serialisierung"]
    D -->|Money| F["Ihr ValueSerializer<Money>"]
    E --> G["JSON-Antwort"]
    F --> G["JSON-Antwort"]

Das Wichtigste hier ist der Gedanke der Verantwortungsgrenze. Der Controller sollte nicht zu einem „Mini-JSON-Generator“ werden, denn dann dupliziert er die Arbeit des Frameworks und wächst. Wir haben in diesem Kurs bereits vereinbart: Der Controller ist dünn, die Logik ist im Service, und die Infrastrukturregeln, einschließlich der JSON-Regeln, leben an den Infrastrukturstellen des Projekts.

3. Serialisierung und Deserialisierung

Während wir hauptsächlich über Antworten nachdenken, scheint Serialisierung das Wort des Tages zu sein. Aber Jackson ist eine bidirektionale Brücke: Es kann nicht nur ein Java-Objekt in JSON schreiben (Serialisierung), sondern auch JSON zurück in ein Java-Objekt lesen (Deserialisierung). In Spring Boot 4 spiegelt sich dies direkt in den Begriffen von Jackson 3 wider: ValueSerializer und ValueDeserializer.

Serialisierung ist unser aktuelles „Brot“: Wir geben CourseCard nach außen, und Jackson verwandelt es in JSON. Deserialisierung wird auftauchen, wenn wir anfangen, JSON vom Client zu empfangen (z.B. im Anforderungstext) oder JSON aus Eingabedaten lesen. In unserem Projekt catalog-service ist der Dienst schreibgeschützt, daher wird Deserialisierung heute eher wie eine „Mechanik, die es zu verstehen gilt“ aussehen, selbst wenn wir sie nicht bei jedem Schritt verwenden.

Um Verwirrung zu vermeiden, behalten Sie ein einfaches Wörterbuch im Kopf:

Begriff Einfach gesagt Wohin „fließt“ es
Serialisierung Java → JSON nach außen, in die HTTP-Antwort
Deserialisierung JSON → Java nach innen, aus HTTP-Anfrage / Datei / Zeichenkette

Und noch ein wichtiger Punkt: Meistens muss der Controller nicht wissen, wie ein bestimmter Typ serialisiert wird. Sonst erhalten Sie eine „Verklebung“ der Web-Schicht mit der Datenformatierung, die später schwer zu ändern ist. Die Idee der heutigen Vorlesung ist genau, den Controller „sauber“ zu halten und das Wissen über das JSON-Format des Typs an einen vorhersehbaren Ort zu verlagern.

Für den aktuellen schreibgeschützten Dienst steht die Serialisierung im Mittelpunkt. Die Deserialisierung ist hier kurz zu sehen, damit der Mechanismus nicht einseitig erscheint, aber man sollte sie nicht zu einer neuen separaten Achse des Projekts machen.

4. Was @JacksonComponent bringt

In „reinem“ Jackson werden benutzerdefinierte Serialisierer normalerweise über ein Jackson-Modul registriert, und dann wird das Modul mit JsonMapper verbunden. Das ist ein normaler Weg, aber er erscheint zu schwerfällig für ein Übungsprojekt, in dem wir eine kleine Regel für einen kleinen Typ haben wollen.

Spring Boot 4 bietet einen kürzeren Weg: Die Annotation @JacksonComponent ermöglicht es, Ihren ValueSerializer/ValueDeserializer als gewöhnlichen Spring Bean zu registrieren, und Boot bindet ihn automatisch an den auto-konfigurierten JsonMapper ein. Wichtig ist, dass @JacksonComponent unter der Haube auch ein @Component ist, also gelten die üblichen Regeln des Component Scans: Die Klasse muss in den gescannten Paketen der Anwendung liegen.

Ein weiterer angenehmer Aspekt: @JacksonComponent kann direkt auf die Implementierung von ValueSerializer oder ValueDeserializer gesetzt werden, oder auf eine „Container“-Klasse mit inneren Klassen des Serialisierers/Deserialisierers. Für Anfänger ist es oft einfacher, mit zwei separaten Klassen zu beginnen: eine schreibt, die andere liest. Boot unterstützt dies.

Und hier kommen wir zum entscheidenden technischen Nutzen. Wenn Sie die Serialisierung für einen Typ einmal beschrieben haben, dann:

  • geben alle Controller weiterhin das Domain-Objekt zurück, nicht den JSON-String;
  • arbeiten alle Services weiterhin mit dem Typ als Typ;
  • wird das JSON-Format für diesen Typ überall dort einheitlich und vorhersagbar, wo Sie bewusst entschieden haben, es anzuwenden.

Das ist einem gut konfigurierten Drucker sehr ähnlich: Sie müssen nicht jedes Mal im Dokument „drucke mit dieser Schriftart und dieser Größe“ schreiben. Sie konfigurieren die Regeln an einem Ort und drucken dann einfach Dokumente.

5. ValueSerializer für Money

Nehmen wir Money als isolierten Demo-Typ. Es ist wichtig, den ValueSerializer selbst zu sehen, also stellen wir uns für einen Moment ein lokales JSON-Szenario vor, in dem dieser Typ als String "19900 GBP" geschrieben werden soll. Dies ist nicht die neue kanonische Form von CourseCard.price in der öffentlichen Antwort des Dienstes; hier betrachten wir bewusst nur die Mechanik des typspezifischen Overrides.

Beginnen wir mit dem Modell selbst. Im Projekt liegt es in catalog.domain. Man kann eine normale Klasse verwenden, aber für ein Übungsprojekt ist ein record sehr praktisch: minimaler Code, maximale Klarheit.

package com.example.catalogservice.catalog.domain;

// Value-Objekt: Betrag + Währung (in der Domäne ist es nicht "nur eine Zahl")
public record Money(int amount, String currency) {
}

Jetzt schreiben wir den Serialisierer. Gemäß der Projektstruktur ist es logisch, ihn in support.jackson zu platzieren, damit sichtbar wird: Dies ist eine Infrastrukturunterstützung, keine Geschäftslogik des Katalogs.

package com.example.catalogservice.support.jackson;

import com.example.catalogservice.catalog.domain.Money;
import org.springframework.boot.jackson.JacksonComponent;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;

import java.io.IOException;

@JacksonComponent
// Serialisieren wir Money in einen kompakten String der Form: "19900 GBP"
public class MoneySerializer extends ValueSerializer<Money> {

    @Override
    public void serialize(Money value, JsonGenerator out, SerializationContext ctx) throws IOException {
        // JsonGenerator ist für das Schreiben des konkreten JSON-Werts in die endgültige Antwort verantwortlich
        out.writeString(value.amount() + " " + value.currency());
    }
}

Beachten Sie einige Dinge. Wir erstellen keinen JsonMapper manuell. Wir greifen nicht in den Controller ein. Wir beschreiben lediglich eine Regel für den Typ Money und markieren die Klasse mit @JacksonComponent. Den Rest erledigt Boot selbst, indem es diesen Bean in Jackson registriert.

Um den Effekt zu sehen, ist es manchmal praktisch, ein kleines technisches „Überprüfungs“-Code-Stück zu erstellen. Nicht im Controller, sondern z.B. in einer @Configuration mit ApplicationRunner. Das ist nicht die „Architektur der Anwendung“, sondern eine Möglichkeit, mit eigenen Augen zu sehen, was Jackson jetzt schreibt.

import com.example.catalogservice.catalog.domain.Money;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tools.jackson.databind.json.JsonMapper;

@Configuration
class JacksonDemoConfiguration {

    @Bean
    ApplicationRunner moneyJsonDemo(JsonMapper jsonMapper) {
        // Demo: JSON so ausgeben, wie es der auto-konfigurierte Jackson mit unseren Komponenten erzeugt
        return args -> System.out.println(
                jsonMapper.writeValueAsString(new Money(19900, "GBP"))
        ); // Erwartet: "19900 GBP"
    }
}

Wenn Sie MoneySerializer deaktivieren, werden Sie höchstwahrscheinlich ein JSON-Objekt wie {"amount":19900,"currency":"GBP"} sehen. Nach dem Aktivieren des Serialisierers wird der String zu "19900 GBP". Und genau hier zeigt sich, warum @JacksonComponent ein mächtiges, aber auch gefährliches Werkzeug ist: Wenn Sie eine solche Regel an einen tatsächlich verwendeten öffentlichen Typ hängen, wird sie überall dort wirken, wo Jackson auf Money trifft. Daher darf eine solche Form für CourseCard nicht zufällig, „nebenbei“, übernommen werden.

6. ValueDeserializer für Money

Unser Hauptweg betrifft immer noch die Antworten, daher sollte die Deserialisierung hier kurz und bündig gezeigt werden. Jackson kann nicht nur ein Objekt in JSON schreiben, sondern auch JSON zurück in ein Objekt lesen. Für den aktuellen schreibgeschützten Dienst wird dies nicht zu einer separaten Achse des Projekts, aber das Prinzip selbst ist jetzt nützlich zu verstehen, während das Beispiel klein ist.

Erstellen wir einen einfachen Deserialisierer, der den String "19900 GBP" liest und in new Money(19900, "GBP") umwandelt. Wir machen das Parsen absichtlich nicht komplex: Es ist ein Übungsbeispiel. In einem echten Projekt würden Sie über Validierung, Leerzeichen, Lokalisierung, negative Werte usw. nachdenken.

package com.example.catalogservice.support.jackson;

import com.example.catalogservice.catalog.domain.Money;
import org.springframework.boot.jackson.JacksonComponent;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.ValueDeserializer;

import java.io.IOException;

@JacksonComponent
// Deserialisieren wir Money aus einem String der Form: "19900 GBP"
public class MoneyDeserializer extends ValueDeserializer<Money> {

    @Override
    public Money deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
        // Eingabe erwartet einen einzelnen JSON-String-Wert, z.B.: "19900 GBP"
        String[] parts = p.getText().split(" ");

        // parts[0] -> amount, parts[1] -> currency (Übungsversion, ohne Validierung)
        return new Money(Integer.parseInt(parts[0]), parts[1]);
    }
}

Jetzt können wir eine kleine „Hin-und-zurück“-Demonstration zeigen. Auch dies ist kein „produktiver Anwendungscode“, sondern eine visuelle Überprüfung im Runner.

import com.example.catalogservice.catalog.domain.Money;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tools.jackson.databind.json.JsonMapper;

@Configuration
class JacksonReadDemoConfiguration {

    @Bean
    ApplicationRunner moneyReadDemo(JsonMapper jsonMapper) {
        // Demo: JSON-String lesen und dank unseres Deserialisierers ein Domain-Money erhalten
        return args -> {
            Money m = jsonMapper.readValue("\"19900 GBP\"", Money.class);
            System.out.println(m); // Money[amount=19900, currency=GBP]
        };
    }
}

Wenn Sie einen Record haben, ist dessen toString() ordentlich, daher wird die Ausgabe ähnlich wie Money[amount=19900, currency=GBP] sein. Wenn es eine normale Klasse ist, wird es so etwas wie com.example...Money@1a2b3c sein, und dann brauchen Sie Getter oder einen ordentlichen toString(). Das ist kein Problem von Jackson – es ist einfach der Unterschied zwischen einem Record und einer „altmodischen Klasse“.

7. Wo Jackson-Komponenten aufbewahren

Es ist sehr leicht, die Serialisierung in „Staub im gesamten Projekt“ zu verwandeln: ein kleiner Serialisierer neben Money, ein anderer neben CourseCard, ein dritter aus irgendeinem Grund im controller, ein vierter im Paket config und ein fünfter überhaupt in „misc“. Wenn Sie den Code dann nach einem Monat öffnen, fühlen Sie sich wie ein Archäologe: „Also… welches Jahrhundert ist das? Frühes Spring oder spätes?“

Um dies zu vermeiden, ist im Projekt catalog-service bereits ein Paket support.jackson vorgesehen. Es ist ideal für solche Dinge: Es handelt sich um eine Infrastrukturunterstützung, die die Darstellung von Daten beeinflusst, aber keine Geschäftslogik des Katalogs ist. Ein solches Paket hilft dem Gehirn: Sie verstehen sofort, dass darin „wie wir mit der Außenwelt über JSON kommunizieren“ liegt, nicht „wie wir featured-Kurse auswählen“.

Der zweite wichtige Punkt ist der Component Scan. Da @JacksonComponent darüber funktioniert, dass es ein Spring Bean ist, muss Ihre Klasse vom Scan gefunden werden. Die Boot-Dokumentation sagt direkt, dass @JacksonComponent ein @Component ist, und daher gelten die Standardregeln des Component Scanning. Wenn Sie den Serialisierer versehentlich außerhalb des Basispakets der Anwendung ablegen, wird er einfach nicht registriert, und Sie werden eine halbe Stunde lang auf das JSON schauen mit dem Gesicht „Warum hörst du mich nicht?“.

Der dritte Punkt ist weniger offensichtlich, aber sehr wichtig: Ein solcher Serialisierer ändert die JSON-Form in der gesamten Anwendung für diesen Typ. Das ist Stärke und Gefahr zugleich. Stärke, weil es einheitlich ist. Gefahr, weil man nicht „in einem Endpunkt so und in einem anderen anders“ machen kann, ohne zusätzliche Mechanismen (das ist ein separates Thema, und wir steigen in diesem Kurs bewusst nicht darauf ein). Wenn Sie also @JacksonComponent hinzufügen, stellen Sie sich immer die erwachsene Frage: „Will ich wirklich, dass dieser Typ in der gesamten Anwendung so aussieht?“ Der String-Money aus diesem Beispiel ist genau eine Demonstration des Mechanismus, nicht die bereits übernommene Form von CourseCard.

8. Wie man die Konfigurationsmethode wählt

Wenn @JacksonComponent auftaucht, hat der Anfänger ein klassisches Auswahlproblem: „Und was mache ich jetzt immer? Die gesamte JSON-Konfiguration darüber?“ Die Antwort enttäuscht meist mit ihrer Reife: „Kommt darauf an.“ Aber lassen Sie uns dieses „kommt darauf an“ technischer machen, damit die Wahl kein Kaffeesatzlesen ist.

Es gibt drei häufigste Konfigurationsebenen. Sie konkurrieren nicht, sie ergänzen sich:

Ansatz Wo lebt die Regel Gut für Kosten eines Fehlers
spring.jackson.* application.yaml globale Flags und einheitliche Richtlinien für das gesamte JSON Sie könnten versehentlich den gesamten Vertrag ändern
Annotation auf dem Modell in der Domain-Klasse kleine, sehr lokale Fälle, wenn Sie bereit sind, das Modell mit JSON zu „verbinden“ Vermischung von Domäne und Darstellung
@JacksonComponent separate Infrastrukturklasse wenn die Regel einen Typ betrifft und überall gelten soll Sie könnten den Typ in der gesamten Anwendung „umfärben“

Die Boot-Dokumentation drängt uns faktisch zu diesem Denken: Zuerst verwenden wir die Autokonfiguration und Eigenschaften, dann gezielte Erweiterungen, und erst dann greifen wir zu schwereren Varianten. Dabei ist @JacksonComponent genau als Alternative zur manuellen Registrierung von Serialisierern über Module beschrieben, um benutzerdefinierte ValueSerializer/ValueDeserializer einfacher anzubinden.

Für catalog-service reicht das aus. Wichtig ist hier eine einfache Disziplin: @JacksonComponent zeigt, wie sich ein bestimmter Typ ändert, während der öffentliche JSON CourseCard separat und bewusst gewählt wird. Wir bauen jetzt keine separate Infrastruktur für verschiedene Formen desselben Typs an verschiedenen Endpunkten und machen die Boot-Baseline nicht zu einem Jackson Deep Dive.

9. Typische Fehler bei der Arbeit mit @JacksonComponent

Fehler Nr.1: Versuchen, JSON direkt im Controller über JsonMapper zu formatieren.
Manchmal scheint es einfacher, return jsonMapper.writeValueAsString(obj) zu schreiben und „alles funktioniert“. Aber Sie verwandeln den Controller in einen Zeichenketten-Generator, brechen die Idee von Spring MVC (Controller gibt Daten zurück, Framework erstellt Antwort), erschweren das Testen und verlieren die Einheitlichkeit. Gezielte Serialisierung sollte in der Jackson-Schicht leben, nicht in Web-Methoden.

Fehler Nr.2: Die Jackson-Komponente außerhalb des Component Scans ablegen und dann fragen, „warum es nicht funktioniert“.
@JacksonComponent funktioniert, weil es ein Spring Bean ist. Wenn die Klasse nicht vom Scan gefunden wird, gelangt sie nicht in den ApplicationContext, und Boot registriert sie nicht im JsonMapper. Dadurch bleibt JSON standardmäßig, und Sie verdächtigen „kaputten Jackson“ oder „seltsames Boot“. In Wirklichkeit ist die Ursache meist einfacher: das falsche Paket.

Fehler Nr.3: Einen Serialisierer erstellen, der „für Menschen schön aussieht“, aber für Daten unpraktisch ist.
Die Form "19900 GBP" ist angenehm für das Auge, aber wenn später eine Sortierung nach Betrag, Filterung nach Währung oder einfach eine strenge Struktur benötigt wird, wird die Zeichenkette zum Problem. Das bedeutet nicht, dass eine Zeichenkette immer schlecht ist – es bedeutet, dass der Serialisierer mit dem Verständnis geschrieben werden muss, dass Sie den Datenvertrag ändern, nicht nur die Kosmetik.

Fehler Nr.4: Einen Deserialisierer „auf die schnelle“ schreiben und die tatsächlichen Eingabedaten vergessen.
split(" ") ist ein hervorragendes Übungsbeispiel, aber in der Praxis gibt es zusätzliche Leerzeichen, leere Zeichenketten, falsche Währungen, nicht-numerische Werte. Wenn Sie Deserialisierung in einem produktiven Szenario verwenden, muss sie robust sein: das Format sorgfältig validieren und eine klare Fehlermeldung geben. Sonst erhalten Sie „wir stürzen irgendwo im JSON-Parser ab“ – eine sehr unangenehme Art von Problemen.

Fehler Nr.5: Erwarten, dass @JacksonComponent „nur für einen Endpunkt“ funktioniert.
Dies ist eine globale Einstellung für den Typ. Wenn Sie einen Serialisierer für Money registriert haben, wird er überall dort angewendet, wo Jackson auf Money trifft. Das ist praktisch für die Einheitlichkeit, aber gefährlich, wenn Sie dachten, „hier soll es ein Objekt sein, dort eine Zeichenkette“. Ein solches Szenario wird mit anderen Werkzeugen gelöst, und im Rahmen dieses Kurses fügen wir sie bewusst nicht hinzu, um die Boot-Baseline nicht in einen Jackson-Deep-Dive zu verwandeln.

1
Aufgabe
Spring Boot, Level 12, Lektion 2
Gesperrt
Punktuelle Serialisierung von `Money` mit `@JacksonComponent`
Punktuelle Serialisierung von `Money` mit `@JacksonComponent`
1
Aufgabe
Spring Boot, Level 12, Lektion 2
Gesperrt
Punktuelle Deserialisierung von `Money` mit `@JacksonComponent`
Punktuelle Deserialisierung von `Money` mit `@JacksonComponent`
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION