CodeGym /Kurse /Spring REST & MVC /@RequestBody und HttpMessageConverter

@RequestBody und HttpMessageConverter

Spring REST & MVC
Level 8, Lektion 0
Verfügbar

1. Woher kommt die „Magie“ von @RequestBody

Wenn Sie zum ersten Mal schreiben: public Something create(@RequestBody Something body), dann versucht Ihr Gehirn ehrlich, das Bild zu vereinfachen: „Na gut, @RequestBody – dann hat Spring irgendwie JSON gelesen und ein Objekt erstellt. Okay.“ Das Problem ist, dass Sie ohne das Verständnis des Zwischenschritts beginnen, Fehler falsch zu diagnostizieren, Ursachen und Folgen zu verwechseln und irgendwann den „kaputten JSON“ mit Prüfungen im Service zu behandeln. Das ist ungefähr so, als würde man eine Erkältung mit der WLAN-Konfiguration behandeln.

Um das Gefühl eines Anfängers zu beschreiben, klingt es so: „Ich habe JSON gesendet. Im Controller erschien ein Objekt. Also parst die Annotation JSON.“ Und hier ist es wichtig, innezuhalten und zu sagen: Die Annotation parst nicht. Die Annotation gibt dem Framework einen Hinweis, woher die Daten kommen. Und parsen muss eine konkrete Komponente, die HTTP-Nachrichten in Java-Objekte und umgekehrt umwandeln kann.

Diese Vorlesung handelt vom „Entzaubern“. Wir steigen nicht in die Quellen von Spring ein (das ist eine eigene Sportart), aber wir vermitteln ein ingenieurtechnisches Bild, das ausreicht, damit Sie MVC nicht mehr als „Sammlung von Annotationen mit zufälligem Verhalten“ wahrnehmen.

Nach den üblichen POST/PUT/DELETE-Endpunkten sind bereits zwei Symptome sichtbar: Das Objekt erscheint irgendwie im Methodenparameter, und kaputter JSON kann schon vor dem Service abgelehnt werden. Das bedeutet, dass zwischen HTTP-Body und Java-Code ein separater Mechanismus lebt. Den untersuchen wir.

HTTP-Body und Java-Objekte: Ein Übersetzer wird benötigt

Innerhalb eines Controllers arbeiten Sie mit Java-Typen: String, UUID (oder String als UUID), Ihren eigenen Klassen usw. Eine HTTP-Anfrage von außen ist dagegen grob gesagt Text und Bytes: Anfragezeile, Header und optional ein Body. Selbst wenn der Body wie „hübscher JSON“ aussieht, ist er für den Server einfach eine Byte-Sequenz, die noch aus dem Eingabestrom gelesen und interpretiert werden muss.

Hier hilft eine Alltagsanalogie sehr: Stellen Sie sich vor, der Client hat Ihnen ein Paket ohne Auspacken geschickt. Im Paket liegt ein sorgfältig zusammengelegter LEGO-Bausatz (Ihr zukünftiges Java-Objekt), aber solange Sie die Box nicht öffnen und die Teile nach Anleitung zusammensetzen, haben Sie kein „Modell“ in den Händen. Der HTTP-Body ist die Box mit Teilen. Das Java-Objekt ist das zusammengebaute Modell. Und jemand muss als „Monteur nach Anleitung“ fungieren.

In Spring MVC übernimmt der Mechanismus HttpMessageConverter die Rolle dieses „Monteurs/Übersetzers“. Er ist verantwortlich für die Konvertierung von HTTP-Anfragen und -Antworten, und zwar nicht nur von JSON: Der String "ok" in der Antwort ist ebenfalls ein Body, und auch er muss in die HTTP-Antwort geschrieben werden.

2. Was macht HttpMessageConverter

Wenn wir unnötige Details entfernen, ist HttpMessageConverter eine Komponente von Spring MVC, die den Body einer HTTP-Anfrage lesen und in ein Java-Objekt umwandeln sowie den Body einer HTTP-Antwort schreiben kann, indem sie ein Java-Objekt in Antwort-Bytes umwandelt. Es handelt sich um eine bidirektionale Brücke: Eingang und Ausgang.

In der offiziellen Dokumentation von Spring Boot/Spring MVC ist dies direkt formuliert: Spring MVC verwendet das Interface HttpMessageConverter, um HTTP-Anfragen und -Antworten umzuwandeln. Dabei gibt es „out of the box“ sinnvolle Defaults: Objekte können automatisch in JSON umgewandelt werden (normalerweise mit Jackson), und Strings werden in UTF‑8 kodiert.

Warum ist dies als separater Mechanismus ausgelagert und nicht in @RequestBody oder @RestController „fest verdrahtet“? Weil der Controller Teil des Vertrags und der Orchestrierung ist (Daten entgegennehmen, Service aufrufen, Antwort zurückgeben), während die Konvertierung eine infrastrukturelle Aufgabe ist. Der Controller muss nicht wissen, wie genau man einen InputStream liest, welcher JSON-Parser verwendet wird, wie man einen String kodiert oder ein Objekt serialisiert. Wenn der Controller das wüsste, würde er schnell zu einem Monster, das „alles“ kann – und ein solches Monster zu unterstützen, wäre schmerzhaft.

Ein weiterer praktischer Vorteil: Dieselbe Controller-Methode könnte (theoretisch) mit verschiedenen Formaten arbeiten, während die konkreten Konvertierungsdetails an die Converter delegiert werden. Die Einzelheiten der Auswahl eines Converters graben wir heute bewusst nicht an – wichtig ist zunächst zu sehen, dass ein Converter existiert und dass er zwischen dem HTTP-Body und Ihrem Parameter/Antwort steht.

3. Die Rolle von @RequestBody in Spring MVC

Jetzt verknüpfen wir die bekannte Annotation mit dem neuen Weltbild. @RequestBody teilt Spring MVC ungefähr Folgendes mit: „Dieser Parameter muss aus dem Body der HTTP-Anfrage bezogen werden.“ Das war’s. Nicht „parse JSON“, nicht „prüfe Felder“, nicht „mache es hübsch“. Einfach: Die Quelle des Werts ist der Body.

Danach startet Spring MVC den entsprechenden Teil der Pipeline: Vor dem Eintritt in Ihre Methode müssen zwei Dinge geschehen. Zuerst muss der Body der Anfrage gelesen werden (der Body sind Bytes). Dann muss dieser Body in ein Java-Objekt des angegebenen Typs umgewandelt werden. Dazu wählt Spring einen passenden HttpMessageConverter aus und bittet ihn, den Body zu „lesen“ und ein Objekt zurückzugeben.

Ein wichtiger ingenieurtechnischer Gedanke hier ist: Wenn Ihre Controller-Methode beginnt, ausgeführt zu werden, ist das @RequestBody-Objekt bereits erstellt. Wenn die Erstellung fehlschlägt (z. B. weil der Body nicht gelesen oder nicht konvertiert werden kann), wird die Controller-Methode möglicherweise überhaupt nicht ausgeführt. Dies ist der Grund, warum kaputter JSON „die Anfrage vor der Servicelogik zerstört“: Sie sind noch gar nicht bis zum Service gekommen, weil der Methodenparameter nicht erstellt werden konnte.

Dieses Verständnis reduziert sofort die Anzahl seltsamer Entscheidungen der Art „lass uns im Service prüfen, ob JSON gültig ist“. Nein, tun Sie das nicht. Wenn Sie bis zum Service gekommen sind, wurde der JSON zumindest bereits in ein Objekt geparst.

4. Konvertierung beim Schreiben der Antwort

Auf der Eingangsseite haben wir es bereits verstanden: @RequestBody startet das Lesen des Bodys. Aber die Konvertierung hat auch eine Kehrseite – das Schreiben der Antwort. In einem @RestController geben Sie oft ein Objekt (oder ResponseEntity) zurück, und Spring muss daraus eine HTTP-Antwort erstellen: Status setzen, Header setzen und (falls erforderlich) den Body schreiben.

Und hier ist es wichtig, zwei nahe beieinanderliegende, aber unterschiedliche Ideen zu trennen. Eine HTTP-Antwort existiert immer: selbst wenn ein Fehler aufgetreten ist oder Sie 204 zurückgegeben haben. Aber der Body in der Antwort kann fehlen. Das Beispiel mit 204 No Content ist eine ideale Illustration: Es gibt eine Antwort, es gibt einen Status, es können Header vorhanden sein, aber es gibt keinen Body. Der „Schritt des Body-Schreibens“ wird also einfach nicht ausgeführt, weil es nichts zu schreiben gibt.

Die offizielle Dokumentation spricht dies nicht so direkt an, aber die Logik folgt aus dem Modell selbst: HttpMessageConverter ist an der Umwandlung von HTTP-Requests/Responses beteiligt, arbeitet also dort, wo ein Body zum Lesen/Schreiben vorhanden ist.

Daher ist es nützlich, ein bidirektionales Schema im Kopf zu behalten: Eingangskonvertierung und Ausgangskonvertierung sind unabhängige Teile. Man kann einen Request-Body lesen, aber keinen Response-Body schreiben (z. B. durch die Rückgabe von 204). Man kann keinen Request-Body lesen (z. B. bei GET), aber einen Response-Body schreiben (ein Objekt als JSON zurückgeben). Und man kann weder das eine noch das andere tun (selten, aber möglich – z. B. ein leerer 204 bei DELETE ohne Request-Body).

5. HttpMessageConverter in der MVC-Kette

Um nicht in Details zu ertrinken, fixieren wir ein „ausreichend genaues“ Schema. Wir werden nicht alle internen Klassen von Spring MVC auflisten, aber es ist wichtig, die Reihenfolge der Ereignisse zu verstehen: Die Anfrage kommt an, Spring wählt die Controller-Methode aus, bereitet die Argumente vor, ruft die Methode auf, nimmt das Ergebnis entgegen und erstellt die Antwort. Der Converter steht an zwei Stellen: beim Lesen des Bodys und beim Schreiben des Bodys.

Unten finden Sie ein Schema in einem Format, das man sich leicht ins Gedächtnis rufen kann, wenn etwas „nicht funktioniert“:

flowchart TD
    %% Converter werden nur dort eingebunden, wo Body tatsächlich gelesen/geschrieben wird
    A["HTTP request"] --> B["Spring MVC: suche Handler-Methode"]
    B --> C{"Müssen Methodenargumente gesammelt werden?"}
    C -->|"mit @RequestBody"| D["HttpMessageConverter: Body lesen -> Java-Objekt"]
    C -->|"ohne Body"| E["Andere Argumente: Path/Query/etc"]
    D --> F["Controller-Methode"]
    E --> F["Controller-Methode"]
    F --> G{"Gibt es einen Body in der Antwort?"}
    G -->|"ja, Objekt zurückgeben"| H["HttpMessageConverter: Java-Objekt -> Antwort-Body"]
    G -->|"nein, 204/void"| I["Antwort ohne Body formen"]

Beachten Sie, dass wir noch nicht besprechen, welcher Converter ausgewählt wird. Das ist ein separates Thema. Unser Ziel ist es jetzt, zu sehen, dass @RequestBody und die „Rückgabe eines Objekts“ einen konkreten Mechanismus aktivieren und nicht durch „Zauberei der Annotation“ funktionieren.

6. Mini-Beispiele zur Konvertierung

Mini-Beispiel Nr. 1: POST /api/v1/tasks – zwei Richtungen der Konvertierung

Das anschaulichste Beispiel ist ein create-Endpoint: Er liest fast immer JSON aus dem Request-Body und gibt fast immer JSON im Response-Body zurück. Das ist wie eine zweispurige Straße: hin und zurück. Und das ist genau der Fall, in dem Ihr @RestController ohne HttpMessageConverter gezwungen wäre, manuell den Request-Strom zu lesen und manuell den Antwort-Strom zu schreiben (der Controller würde zu einem Mini-Servlet werden).

Wir beginnen mit minimalen „Körpern“ von Anfrage/Antwort. Ich mache sie absichtlich primitiv, damit wir uns auf die Mechanik konzentrieren und nicht auf die Schönheit des Modells.

// DTO des Anforderungskörpers zur Aufgabenerstellung (kommt aus dem Request-Body)
class CreateTaskBody {
    // Titel der Aufgabe (erwartet Feld "title" in JSON)
    public String title;

    // Beschreibung der Aufgabe (erwartet Feld "description" in JSON)
    public String description;
}
// DTO des Antwortkörpers (geht in den Response-Body)
class TaskResponseBody {
    // ID der Aufgabe (zur Veranschaulichung als String)
    public String id;

    // Titel der Aufgabe
    public String title;
}

Und jetzt – die Controller-Methode selbst. Sie sieht sehr „normal“ aus, und genau darin liegt ihre Stärke: Sie enthält keine einzige Zeile über JSON.

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
class TaskController {

    @PostMapping("/api/v1/tasks")
    public TaskResponseBody create(@RequestBody CreateTaskBody body) {
        // An diesem Punkt ist body bereits KEIN JSON-String mehr:
        // Spring MVC hat bereits HttpMessageConverter aufgerufen und das Objekt CreateTaskBody erstellt
        TaskResponseBody response = new TaskResponseBody();

        // In einem echten Projekt wird die ID normalerweise von der DB oder dem Service erstellt; hier ein Platzhalter
        response.id = "t-1";

        // Einfaches Kopieren der Daten aus dem Request-DTO in das Response-DTO
        response.title = body.title;

        // Objekt zurückgeben: Danach wird Spring MVC wieder HttpMessageConverter aktivieren,
        // diesmal jedoch zum Schreiben des Response-Bodys
        return response;
    }
}

Was wirklich passiert (aber ohne Mystik): Spring MVC erhält die HTTP-Anfrage. Es sieht, dass das Methodenargument mit @RequestBody markiert ist. Also muss es den Body lesen, ihn in CreateTaskBody umwandeln und erst dann create(...) aufrufen. Nach dem return sieht Spring MVC, dass die Methode ein Objekt (nicht void) zurückgegeben hat, und muss dieses Objekt in den Response-Body schreiben. Dazu wird erneut ein Converter aufgerufen, nur diesmal „in die umgekehrte Richtung“.

Wenn Sie jetzt eine Anfrage wie die folgende senden:

# Beispiel einer Client-Anfrage (was tatsächlich über das Netzwerk gesendet wird)
POST /api/v1/tasks HTTP/1.1
Content-Type: application/json

{
  "title": "Fix API",
  "description": "Explain body conversion"
}

dann wird Ihre Methode JSON überhaupt nicht als Text sehen. Sie sieht ein bereits erstelltes Objekt. Das ist der wichtigste psychologische Wendepunkt: Der Controller parst kein JSON – er arbeitet mit dem Ergebnis des Parsens.

Mini-Beispiel Nr. 2: DELETE und 204 No Content – Antwort vorhanden, Body nicht, und das ist in Ordnung

Das zweite nützliche Beispiel ist ein delete-Szenario. Es ist gut, weil man sofort sieht: HTTP-Antwort und Antwort-Body sind nicht dasselbe. Und übrigens: Ihr Controller muss auch nicht „weil REST“ JSON zurückgeben. Manchmal ist die ehrlichste Form der Antwort das Fehlen eines Bodys.

Hier ist ein typischer DELETE:

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;

@DeleteMapping("/api/v1/tasks/{taskId}")
public ResponseEntity<Void> delete(@PathVariable String taskId) {
    // Vertrag: Wir geben nichts im Body zurück, nur Status 204
    // Daher wird der Schritt "Response-Body über HttpMessageConverter schreiben" hier nicht benötigt
    return ResponseEntity.noContent().build();
}

Hier gibt die Methode eine „Antwort ohne Body“ zurück. Spring wird eine HTTP-Antwort mit Status 204 erstellen, aber der Schritt „Objekt in den Response-Body schreiben“ entfällt. Und das ist wichtig zu bedenken, wenn Sie diagnostizieren: „Warum sieht der Client kein JSON?“ Manchmal sieht er es nicht, weil Sie es ehrlich nicht gesendet haben.

Dieses Beispiel heilt psychologisch auch die Abhängigkeit von „gib immer JSON zurück“. Nein. Geben Sie das zurück, was dem Vertrag entspricht. Wenn der Vertrag 204 No Content besagt, dann gibt es in der Antwort tatsächlich keinen Inhalt. Nicht „leerer JSON“, nicht {}, sondern wirklich das Fehlen eines Bodys.

Mini-Beispiel Nr. 3: GET /ping und String – Konvertierung der Antwort ist nicht nur JSON

Um endgültig die Illusion „Converter = JSON“ zu beseitigen, ist es hilfreich, ein Beispiel mit einfachem Text zu sehen. Denn auch ein String im HTTP-Body muss von jemandem geschrieben werden. Und in Spring MVC geschieht das wieder über einen HttpMessageConverter – nur von einem anderen Typ.

Hier ist der einfachste Endpunkt:

import org.springframework.web.bind.annotation.GetMapping;

@GetMapping(path = "/ping", produces = "text/plain")
public String ping() {
    // Wir geben einen String zurück, aber im HTTP wird dies zum Response-Body (ein Satz Bytes, normalerweise UTF-8).
    // Auch dieses Schreiben übernimmt ein HttpMessageConverter, nur kein JSON-Converter.
    return "ok";
}

Aus Sicht des Controllers ist das einfach ein String. Aber aus Sicht des HTTP ist es ein Antwort-Body: eine Byte-Sequenz in einer bestimmten Kodierung (in der Regel UTF‑8). Spring MVC enthält genau einen Satz „sinnvoller Defaults“, einschließlich der Arbeit mit Strings und grundlegenden Formaten.

Dieses Beispiel ist auch methodisch nützlich: Wenn Sie die Antwort "ok" sehen, können Sie sie nicht mehr mit „Jackson parst JSON“ erklären. Hier gibt es kein JSON. Es muss also eine allgemeinere Abstraktion existieren. Und sie existiert: Message Converter.

7. Typische Fehler: @RequestBody und Converter

Wenn Sie gerade erst mit Spring MVC anfangen, liegen die Fehler meistens nicht im Code, sondern im mentalen Modell. Und das ist normal: Das Gehirn versucht, alles als direkten Methodenaufruf darzustellen, aber in Wirklichkeit findet vor dem Methodenaufruf eine ganze Vorbereitung statt. Unten sind die häufigsten Fallstricke genau zum Thema „Wo ist die Konvertierung und warum ist sie keine Magie“.

Fehler Nr. 1: Zu denken, dass @RequestBody selbst JSON parst.
Wegen dieser Idee beginnen die Leute, die „Annotation zu reparieren“: Sie fügen im Service überflüssige Umwandlungen hinzu, schreiben manuell ObjectMapper im Controller, lesen HttpServletRequest.getInputStream() – alles nur, um „Spring zu helfen“. In Wirklichkeit ist @RequestBody nur eine Markierung der Quelle, und die Arbeit erledigt HttpMessageConverter. Wenn Sie ein Objekt als Parameter annehmen, ist es bereits das Ergebnis der Konvertierung, und ab sofort müssen Sie nicht in „JSON-Strings“ denken, sondern in Datenstrukturen.

Fehler Nr. 2: Zu erwarten, dass Body immer sowohl in der Anfrage als auch in der Antwort vorhanden ist.
Nach ein paar erfolgreichen POST gewöhnt man sich leicht an „REST = JSON überall“. Dann schreiben Sie einen DELETE, geben 204 zurück, und der Client „sieht keine Antwort“. Aber er soll auch keinen Body sehen: 204 No Content bedeutet das Fehlen eines Bodys. Es ist wichtig, zwischen „HTTP-Antwort“ und „Body der HTTP-Antwort“ zu unterscheiden: Status und Header sind da, Body kann fehlen, und das ist ein gültiger Vertrag.

Fehler Nr. 3: Konvertierungszeitpunkt und Geschäftslogik zu vermischen.
Ein sehr häufiger Schmerz ist, wenn eine Anfrage fehlschlägt und Sie zum Debuggen in den Service gehen. Aber wenn das Problem beim Lesen des Bodys lag, wurde der Service einfach nicht aufgerufen. Das ist ein Schlüsselmerkmal: Die Konvertierung des Bodys erfolgt vor dem Eintritt in die Controller-Methode. Ein Teil der Fehler ereignet sich also „vor Ihnen“ – und genau das wollen wir erkennen können.

Fehler Nr. 4: Zu versuchen, den Request-Body manuell zu lesen, wenn @RequestBody vorhanden ist.
Manchmal möchte man „zur Sicherheit“ den Body loggen oder noch einmal lesen. Aber der Body einer Anfrage ist ein Strom, und wenn Sie ihn manuell gelesen haben, hat der Converter möglicherweise nichts mehr zu lesen. Das Ergebnis sind seltsame Fehler, die aussehen wie „Spring sieht kein JSON“. In der Praxis gilt eine einfache Regel: Entweder Sie vertrauen @RequestBody und dem Converter, oder Sie gehen in die Low-Level-Welt der manuellen Verarbeitung. In einem Lernprojekt und in den meisten realen REST-APIs ist der erste Weg fast immer der richtige.

Fehler Nr. 5: Zu glauben, dass Spring „alles selbst weiß“ und nicht über die Phasen nachzudenken.
Das Wort „Autokonfiguration“ macht manchmal zu sehr entspannt. Ja, in Spring Boot ist vieles „standardmäßig“ eingestellt, und das ist praktisch. Aber ingenieurtechnisch ist es wichtig zu bedenken: Auch wenn Sie nichts konfiguriert haben, existiert der Mechanismus trotzdem. Spring MVC verwendet HttpMessageConverter zur Umwandlung von Anfragen und Antworten, und es gibt Standardregeln, einschließlich der automatischen Umwandlung von Objekten in JSON über Jackson und der Arbeit mit Strings.

1
Aufgabe
Spring REST & MVC, Level 8, Lektion 0
Gesperrt
Echo von JSON-Nachrichten über `@RequestBody`
Echo von JSON-Nachrichten über `@RequestBody`
1
Aufgabe
Spring REST & MVC, Level 8, Lektion 0
Gesperrt
Textantwort und Antwort ohne Inhalt
Textantwort und Antwort ohne Inhalt
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION