1. Multipart vs. JSON: Was sich ändert
Wenn Sie ein paar Tage am Stück in der Welt von REST und JSON gelebt haben, gewöhnt sich das Gehirn daran: Eine Anfrage ist ein einzelnes JSON-Dokument, das elegant in ein DTO über @RequestBody gemappt wird. Multipart bricht diese Gewohnheit bewusst und ohne Entschuldigung: Es ist ein Format, bei dem die Anfrage aus „Teilen“ besteht, wie eine Schachtel mit Fächern. Wenn man diesen Unterschied nicht akzeptiert, kommen bald seltsame Fragen wie „Warum funktioniert @RequestBody MultipartFile nicht?“ – und ja, diese Frage haben fast alle gestellt, mich eingeschlossen (ich war jung, naiv und glaubte an die Universalität von @RequestBody).
JSON vs. multipart: Einfach erklärt
Eine JSON-Anfrage hat normalerweise ein offensichtliches Szenario: Wir senden ein einzelnes Dokument.
Das Szenario einer Multipart-Anfrage ist anders: Wir senden mehrere Teile in einer einzigen Anfrage. Ein Teil kann eine Datei sein, ein anderer ein Textfeld, ein weiterer etwas anderes. Für einen Avatar ist das natürlich: Datei + möglicherweise irgendein Text wie "source=mobile-app" oder "source=web".
Eine kleine Vergleichstabelle hilft, es zu verinnerlichen:
| Vergleichspunkt | application/json | multipart/form-data |
|---|---|---|
| Wie sieht der Anfragetext aus? | ein einzelnes JSON-Dokument | eine Sammlung von Teilen |
| Typische Daten | DTOs, Befehle, Filter | Dateien + Formularfelder |
| Wohin wird es im Controller gemappt | @RequestBody SomeDto | @RequestParam MultipartFile file |
| Normale Rolle | Geschäftsanweisung | Transport für Datei(en) |
Und wichtig: Multipart ist nicht „etwas innerhalb von Spring“, es ist ein normales HTTP-Format. Spring hilft Ihnen nur dabei, die Grenzen und Bytes nicht von Hand parsen zu müssen (sonst würden wir alle sehr traurig leben).
2. Weg der Multipart-Anfrage in Spring MVC
Die gute Nachricht: Spring MVC zwingt Sie nicht, Multipart manuell zu parsen. Die schlechte Nachricht: Es ist keine Magie, sondern eine Kette von Schritten – und wenn Sie nicht verstehen, wo Sie sich gerade befinden, werden Sie am „Falschen“ debuggen. Besonders lustig wird es, wenn die Anfrage wegen der Security-Schicht (z.B. wegen CSRF) den Controller nicht erreicht und Sie versuchen, MultipartFile zu „reparieren“. Es ist, als würde man den Postboten beschimpfen, weil man nicht in den Hauseingang gelassen wurde.
Vereinfachtes Schema des Anfragewegs
Zeichnen wir ein maximal anwendungsnahes Schema (ohne Eintauchen in die „Eingeweide der Eingeweide“):
flowchart TD
A["Client sendet POST multipart/form-data"] --> B["Servlet container (Tomcat)"]
B --> C["Spring Security filters"]
C --> D["DispatcherServlet (Spring MVC)"]
D --> E["Multipart parsing / MultipartResolver"]
E --> F["Binding der Controller-Methodenargumente"]
F --> G["@RequestParam MultipartFile file"]
Der Sinn hier ist folgender:
Zuerst gelangt die Anfrage in den Servlet-Container (z.B. Tomcat), dann durchläuft sie die Filter von Spring Security. Wenn dort alles in Ordnung ist (Benutzer angemeldet, CSRF ok – dazu später mehr), erreicht die Anfrage den DispatcherServlet. Und erst dann, in der Spring-MVC-Phase, wird die Anfrage als Multipart erkannt, die Teile werden extrahiert, und dann kann Spring das benötigte MultipartFile in Ihre Controller-Methode einsetzen.
Was für das Debuggen wichtig ist:
Wenn Ihre Controller-Methode nicht aufgerufen wird, bedeutet das möglicherweise nicht, dass „MultipartFile nicht funktioniert“, sondern dass die Anfrage früher „gestorben“ ist. Zum Beispiel bedeutet 403 Forbidden aufgrund von CSRF, dass MultipartFile hier überhaupt nicht relevant ist: Zuerst muss man verstehen, warum die Security eine Statusänderungs-Anfrage nicht weitergeleitet hat.
3. Upload-Endpunkt im Controller
Wenn Sie einen Upload-Endpunkt schreiben, tun Sie zwei wichtige Dinge: Erstens zeigen Sie explizit in der Signatur, dass es sich um Multipart handelt (und nicht „es kommt irgendetwas an und wir raten hinterher“). Zweitens verknüpfen Sie spezifische Teile der Multipart-Anfrage mit den Methodenparametern. Dies ist ein Fall, in dem „Explizitheit“ wirklich Lebensstunden spart: für Sie, Ihr zukünftiges Ich und den Teamleiter, der nicht in Kaffeesatz lesen möchte.
Minimaler Endpunkt für einen Avatar
Beginnen wir mit einer Form, die man als „Datei-Upload“ liest:
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class MeAvatarController {
@PostMapping(path = "/api/me/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void uploadAvatar(@RequestParam("file") MultipartFile file) {
// Wichtig: "file" ist der Name des Parts in der Multipart-Anfrage. Der Client muss diesen Teil genau so senden.
// In der Praxis führt man hier fast immer eine frühe Validierung durch (z.B. file.isEmpty()).
// später rufen wir den Service auf und speichern die Datei
}
}
Es gibt zwei wesentliche Punkte hier.
consumes = multipart/form-data macht den Endpunkt „selbstdokumentierend“: Wir sagen Spring (und dem Menschen) explizit, dass hier kein JSON gesendet werden kann.
@RequestParam("file") ist nicht weniger wichtig: Der String "file" ist der Part-Name. Der Client muss die Datei genau unter diesem Namen senden. Wenn der Client avatar anstelle von file sendet, wird Spring nicht „raten“, sondern ehrlich sagen: „Ich habe keinen Teil file“.
Multipart kann sowohl eine Datei als auch Textfelder enthalten
Manchmal ist es nützlich, noch ein Feld aufzunehmen – einfach um zu spüren, dass Multipart wirklich „aus Teilen besteht“:
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class MeAvatarController {
@PostMapping(path = "/api/me/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void uploadAvatar(@RequestParam("file") MultipartFile file,
@RequestParam("source") String source) {
// file = der binäre Teil (die Datei selbst)
// source = der Textteil (eine normale Zeichenkette aus dem multipart-form)
// Wichtig: die Namen der Parts ("file" und "source") sind der Vertrag mit dem Client.
}
}
Diese Version ist nützlich als „Lehrröntgen“: Sie sehen, dass eine Anfrage sowohl eine Datei als auch einfache Felder bringen kann.
4. MultipartFile: Was es ist und wie man es liest
MultipartFile ist ein Objekt, das Spring MVC Ihnen als „Repräsentation der hochgeladenen Datei“ gibt. Es ist kein File aus dem Dateisystem und keine „Garantie, dass die Datei für immer auf der Festplatte liegt“. Es ist eher ein „Zugriffscontainer“ für Inhalt und Metadaten: Größe, angegebener Content-Type, ursprünglicher Name und eine Möglichkeit, die Bytes zu lesen. Wenn Sie es als Domänenentität behandeln, wird es weh tun; wenn Sie es als Boundary-Objekt betrachten, wird alles gut und ruhig sein.
Nützliche Methoden von MultipartFile
Hier ist eine kleine Liste dessen, was Sie in diesem Kurs tatsächlich verwenden werden:
| Methode | Rückgabetyp | Verwendung in unserem Szenario |
|---|---|---|
| isEmpty() | Datei vorhanden/nicht vorhanden | frühe Validierung: „wurde überhaupt etwas gesendet?“ |
| getName() | Name des Parts | normalerweise "file" |
| getOriginalFilename() | ursprünglicher Dateiname | für Logs/Diagnose (aber nicht als Pfad!) |
| getContentType() | angegebener MIME-Typ | erster Filter: „sieht es nach einem Bild aus?“ |
| getSize() | Größe in Bytes | Größenlimit des Avatars |
| getInputStream() | Eingabestream | sicherer als alles in den Speicher zu laden |
| transferTo(...) | Datei speichern | bequem, aber wir verwenden es bewusst |
Mini-Beispiel: Grundinformationen extrahieren
Wir erstellen eine kleine Dienstmethode, die die Datei „beschreibt“. Das ist keine endgültige Avatar-Logik (die kommt später), sondern ein diagnostischer Schritt, damit Sie verstehen, was in MultipartFile eigentlich steckt:
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
public class AvatarInspector {
public String describe(MultipartFile file) {
// Wichtig: getName() ist der Name des Parts (z.B. "file"), nicht der Dateiname auf der Festplatte.
// getOriginalFilename() ist der originale Name vom Client (für Logs/Diagnose, nicht für Pfade).
return ("""
name=%s, original=%s, type=%s, size=%d
""".strip())
.formatted(
file.getName(),
file.getOriginalFilename(),
file.getContentType(),
file.getSize()
);
}
}
Beachten Sie den Unterschied zwischen getName() und getOriginalFilename(). getName() ist der Name des Teils (normalerweise "file"), während getOriginalFilename() der vom Client angegebene Dateiname ist (avatar.png). Und dem zweiten darf man nicht als „Pfad“ vertrauen – aber das ist eine andere Geschichte über sicheres Speichern.
Gefährliche Angewohnheit getBytes()
MultipartFile hat die Methode getBytes(). Sie scheint praktisch, weil „gib mir die Bytes, fertig“. Aber sie hat eine unangenehme versteckte Bedeutung: „lege die gesamte Datei auf einmal in den Speicher“. Für einen Avatar ist das vielleicht noch tolerierbar (wenn Sie die Größe begrenzt haben), aber die Angewohnheit ist schlecht. In realen Projekten bauen Leute so versehentlich einen Mini-DDoS auf ihren eigenen Dienst: eine große Datei – und tschüss, OutOfMemoryError.
Daher sollte man sich die Regel im Kopf behalten: Wenn die Chance besteht, dass die Datei merklich groß ist, denken Sie in Streams (getInputStream()), Limits und sorgfältiger Speicherung, nicht in „alles in ein byte[]“.
5. Überprüfung, ob Multipart bis zum Code gelangt
In diesem Stadium möchte man einfaches menschliches Glück: eine Anfrage senden, sehen, dass MultipartFile nicht null ist, und dass Größe/Name/Typ gelesen werden können. Das ist ein wichtiger Schritt, denn später werden wir CSRF-Nuancen hinzufügen, und wenn Sie sich nicht sicher sind, dass der Basis-Upload überhaupt funktioniert, werden alle 403/415 wie ein großer Nebel erscheinen. Unser Ziel ist es, die Ursachen zu trennen: „Multipart wurde nicht zusammengesetzt“ und „Security hat nicht zugelassen“.
Vorübergehende diagnostische Antwort
Man kann vorübergehend ein einfaches DTO/Record mit Dateiinformationen zurückgeben. Das ist ein didaktischer Trick: Sie bestätigen visuell, dass der Upload funktioniert.
// Einfache Diagnoseantwort: Wir geben zurück, was tatsächlich im multipart angekommen ist.
public record AvatarUploadInfo(String originalName, String contentType, long size) {
}
Und der Controller:
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class MeAvatarController {
@PostMapping(path = "/api/me/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public AvatarUploadInfo uploadAvatar(@RequestParam("file") MultipartFile file) {
// Diagnose: Wir geben Metadaten zurück, um sicherzustellen, dass das Multipart-Binding funktioniert.
// Wichtig: Der originale Dateiname ist kein "sicherer Dateiname auf dem Server".
return new AvatarUploadInfo(file.getOriginalFilename(), file.getContentType(), file.getSize());
}
}
Ja, hier macht der Controller etwas mehr, als er sollte. Für die Diagnose ist das in Ordnung: Sie bestätigen visuell, dass das Multipart-Binding überhaupt funktioniert. In einem Endpunkt im Produktionsbetrieb verschwindet diese Diagnoseantwort später, und der Controller bleibt wieder ein schlanker Einstiegspunkt.
Beispiel einer Anfrage mit curl
Technisch gesehen ist der einfachste Weg, eine Multipart-Anfrage zu senden:
# -F erzeugt multipart/form-data.
# "file=@./avatar.png" bedeutet: part mit dem Namen file, und der Wert ist der Inhalt der Datei avatar.png.
curl -X POST \
-F "file=@./avatar.png" \
-F "source=curl" \
http://localhost:8080/api/me/avatar
Wenn Sie eine 403 mit einem Hinweis auf CSRF erhalten – das ist in unserem Stateful-Modell erwartbar. Das ist ein gutes Signal: Das Multipart-Binding ist nicht mehr das Hauptproblem; weiter muss man schauen, wie der Client den Statusänderungs-Request innerhalb derselben Sitzung bestätigt.
6. Grenzen von Multipart in Spring Boot
Spring Boot kann Multipart-Uploads auf Plattformebene begrenzen. Es ist schon jetzt nützlich, sich daran zu erinnern, aus einem Grund: Ein zu großer Upload kann noch vor dem Controller sterben, und dann wird Ihr AvatarService garnicht erst anfangen zu arbeiten.
Für einen Avatar ist dies genau ein Plattform-Level-Schutz, keine Geschäftsregel. Sie beantwortet die Frage „Lassen wir die Anwendung überhaupt eine solche Anfrage verarbeiten“, und nicht „welche Größe wir für einen Avatar als normal ansehen“. Daher führt eine zu große Anfrage manchmal nicht zu einer schönen Anwendungsfehlermeldung, sondern zu einer frühen Ablehnung auf Infrastrukturebene.
Vorerst reicht es, sich eines zu merken: Wenn Multipart nicht bis zum Code gelangt, kann die Ursache nicht nur in der Security liegen, sondern auch in den Grenzen des Upload-Mechanismus selbst. Die Größeregeln des Avatar-Szenarios sollten besser separat und direkt in der Anwendung festgelegt werden.
7. Typische Fehler bei der Arbeit mit Multipart
Fehler Nr. 1: Versuch, eine Datei über @RequestBody zu empfangen.
Die Intuition nach JSON-Endpunkten sagt: „Da es einen Anfragetext gibt, also @RequestBody“. Aber Multipart ist eine Sammlung von Teilen, und Spring mappt die Datei nicht über den HttpMessageConverter für JSON, sondern über die Multipart-Mechanik. Daher ist der korrekte Einstiegspunkt @RequestParam MultipartFile file (oder ähnliche Varianten), nicht @RequestBody.
Fehler Nr. 2: consumes = multipart/form-data vergessen und sich dann lange mit 415 Unsupported Media Type herumschlagen.
Manchmal funktioniert der Endpunkt „so, als ob er funktioniert“ ohne consumes, und manchmal bricht er je nach Client und Headern. In einem Lernprojekt ist es besser, sich an Explizitheit zu gewöhnen: Ein Upload-Endpunkt sollte ehrlich von sich sagen, dass er Multipart ist. Das verringert die Wahrscheinlichkeit, dass Sie versehentlich JSON dorthin senden und debuggen, „warum Spring die Datei nicht sieht“.
Fehler Nr. 3: Der Part-Name in der Anfrage stimmt nicht mit dem Namen in @RequestParam überein.
Sie schreiben @RequestParam("file"), aber Postman sendet das Feld avatar. Spring errät nichts und improvisiert nicht, es findet einfach den benötigten Teil nicht. Dies wird nicht durch „Spring-Einstellungen“ behoben, sondern durch Vertragsdisziplin: Auf "file" geeinigt – Dann senden wir "file".
Fehler Nr. 4: file.isEmpty() wird nicht überprüft und man wundert sich, dass „man angeblich eine Datei gesendet hat, aber sie leer ist“.
Eine Multipart-Anfrage kann einen Teil mit dem Namen file enthalten, aber ohne Daten (oder der Client könnte ein Formular ohne ausgewählte Datei gesendet haben). Für einen Avatar ist das ein typischer Anwendungsfehler, und man sollte ihn früh abfangen. Sogar vor allen Security-Nuancen und vor dem Speichern auf die Festplatte.
Fehler Nr. 5: Verwechslung von getName() und getOriginalFilename().
getName() ist der Name des Parts (normalerweise "file"), während getOriginalFilename() das ist, was der Client gesendet hat (z.B. "C:\Users\Bob\Desktop\virus.png"… ja, das kommt auch vor). Wenn Sie getOriginalFilename() als Teil des serverseitigen Pfads verwenden, öffnen Sie die Tür zu Problemen. Heute reicht es, sich eine einfache Regel zu merken: Der ursprüngliche Name ist maximal für die Diagnose, aber nicht für die Pfaderstellung.
GO TO FULL VERSION