1. Die Aufgabe des Binding in Spring MVC
Wenn es Ihnen früher vorkam, dass @RequestParam int page Magie auf dem Niveau „Spring kann Gedanken lesen“ ist, dann zerlegen wir diese Magie jetzt sorgfältig in ihre Bestandteile. Die Schlüsselidee ist einfach: HTTP bringt kein int, keine LocalDate oder TaskStatus in Ihre Anwendung. Es bringt Zeichen. Also eigentlich einen String. Und erst dann versucht Spring zu verstehen, in was daraus gemacht werden soll.
Stellen Sie sich eine typische Anfrage an unsere Aufgabenliste vor:
GET /api/v1/tasks?page=1&size=20&status=IN_PROGRESS&dueBefore=2026-03-21
Aus HTTP-Sicht ist das nur eine Sammlung von Schlüssel=Wert-Paaren, wobei die Werte Text sind. Sogar 1 und 20 sind keine Zahlen, sondern Zeichenfolgen "1" und "20". Sogar ein Datum ist ein String "2026-03-21". Sogar ein Status ist ein String "IN_PROGRESS".
Und hier ist es wichtig: Der Parametertyp in der Controller-Methode ist ein Teil des Vertrags. Sobald Sie TaskStatus status geschrieben haben, haben Sie dem Kunden gesagt: „Sende nicht „irgendetwas“, sondern einen der zulässigen Statuswerte“. Wenn Sie LocalDate dueBefore geschrieben haben, haben Sie gesagt: „Das Datum muss im erwarteten Format vorliegen, sonst gilt die Anfrage nicht als korrekt“.
Ungewohnt? Ja. Nützlich? Auch ja. Denn das macht die API vorhersagbar: Wir raten nicht, was der Kunde gemeint hat, sondern sagen ehrlich und früh „ja/nein“.
2. Wo findet die Parameterumwandlung statt
Jetzt kommt ein wichtiger Punkt, der oft übersehen wird: Die Umwandlung erfolgt bevor Ihre Controller-Methode ausgeführt wird. Das heißt, Sie können einen perfekten Service schreiben und mit Logik füllen, aber wenn der Kunde page=kotik sendet, wird Ihr Service diese Anfrage nicht einmal sehen. Spring stoppt sie am Eingang.
Um dies zu verinnerlichen, kann man sich eine Mini-Pipeline der Parameterverarbeitung so vorstellen:
flowchart TD
A["HTTP-Anfrage"] --> B["Spring extrahiert rohe Werte aus Path/Query/Header/Cookie"]
B --> C["Type Conversion / Formatting ConversionService"]
C -->|Erfolg| D["Aufruf der Controller-Methode mit typisierten Argumenten"]
C -->|Fehler| E["Umwandlungsfehler: Controller-Methode wird NICHT aufgerufen"]
Diese Kette wird später auch funktionieren, wenn wir statt einer Reihe einzelner Parameter die Query-Zeichenfolge in ein einziges Criteria-Objekt zusammenfassen: Die Form des Arguments ändert sich, nicht die Mechanik des Bindings.
Um es etwas erwachsener auszudrücken: Spring MVC verwendet für Parameter aus Path/Query/Header/Cookie den Mechanismus des Type Conversion basierend auf ConversionService und Formatierern. Dies ist nicht dasselbe wie die Konvertierung von JSON-Body – dort kommt ein HttpMessageConverter zum Einsatz, aber das ist ein Thema für einen anderen Tag. Hier geht es genau um die einfachsten Werte aus der Anfrage, die standardmäßig als Text eingehen.
Warum ist das speziell für Sie als API-Entwickler wichtig? Weil Sie den Controller nicht mehr als Startpunkt betrachten. Der Startpunkt ist die API-Grenze, und der Controller ist bereits Teil der verarbeiteten Anfrage. Das heißt, „etwas ist schiefgelaufen“ kann noch vor Ihrem taskService.findTasks(...) passieren.
3. Binding von enum über TaskStatus
Enums sind eine der angenehmsten Möglichkeiten, einen Vertrag ehrlich zu gestalten. Sie funktionieren wie eine U-Bahn-Sperre: Ohne Ticket, also ohne gültigen Wert, kommt man nicht durch. Und das ist keine Grausamkeit, sondern Fürsorge für Ihr zukünftiges Ich und Ihre Kunden: Je weniger Werte „nach Fantasie“, desto weniger Überraschungen.
Beginnen wir damit, dass wir in der Domain bereits einen festen Satz von Status haben. Im Projekt ist das TaskStatus. Lassen Sie uns das Enum explizit zeigen:
package com.example.tasktracker.domain.model;
/**
* Status der Aufgabe.
* Wichtig: Beim Standard-Binding erwartet Spring den exakten Konstantennamen (Groß-/Kleinschreibung ist wichtig).
*/
public enum TaskStatus {
TODO, // Aufgabe erstellt, aber noch nicht in Bearbeitung
IN_PROGRESS, // Aufgabe in Bearbeitung
BLOCKED, // Aufgabe durch externe Umstände blockiert
DONE, // Aufgabe erledigt
ARCHIVED // Aufgabe „aus den Augen genommen“, aber nicht gelöscht
}
Jetzt möchten wir, dass der Client die Aufgabenliste aufrufen und optional nach Status filtern kann. Auf Controller-Ebene sieht das sehr natürlich aus: Der Status ist ein Query-Parameter und optional.
import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("/api/v1/tasks")
public Object findTasks(
// Spring konvertiert den String aus dem Query (?status=IN_PROGRESS) selbstständig in den Enum TaskStatus
// Wenn der Parameter nicht gesendet wurde, wird null sein (Filter wird nicht angewendet)
@RequestParam(required = false) TaskStatus status
) {
return taskService.findByStatus(status);
}
Beachten Sie, wie schön das im Service wird: Er erhält entweder null (kein Filter) oder einen echten Enum, keinen String, den man manuell mit "TODO" vergleichen und hoffen muss, dass man sich nirgends vertippt hat.
Für manuelle Tests können Sie eine .http-Anfrage hinzufügen oder in Postman dasselbe machen:
### Filter tasks by status
GET http://localhost:8080/api/v1/tasks?status=IN_PROGRESS
Accept: application/json
Jetzt zu einer wichtigen Nuance, die jeden Anfänger unweigerlich erwischt. Das Enum-Binding ist standardmäßig case-sensitiv und erwartet den exakten Konstantennamen. Also so funktioniert es:
GET http://localhost:8080/api/v1/tasks?status=TODO
Aber so wird es höchstwahrscheinlich schon vor dem Controller fehlschlagen:
GET http://localhost:8080/api/v1/tasks?status=todo
Spring wird nicht raten, was Sie sagen wollten. Es wird ehrlich sagen: „Kann den String todo nicht in TaskStatus umwandeln“. Das ist ein strenger Vertrag. In der kommerziellen Entwicklung ist das in Ordnung: Sie dokumentieren die zulässigen Werte und die Kunden halten sich daran. Wenn Sie sowohl todo, TODO als auch ToDo akzeptieren möchten, ist das eine separate Entscheidung für tolerante Eingaben und erfordert einen bewussten Ansatz.
4. Binding von Zahlen: page und size
Bei Zahlen scheint alles einfach, bis jemand size=mnogo sendet. Und dann freuen Sie sich plötzlich, dass Spring die Anfrage an der Grenze stoppt, anstatt dieses „viel“ zum Service durchzulassen. Numerische Parameter sind Klassiker von List-Endpunkten und es ist praktisch, sie streng typisiert zu machen.
In der aktuellen Phase implementieren wir noch keine echte Paginierung, aber der Vertrag kann bereits klar gemacht werden. Nehmen wir an, standardmäßig sind page = 0, size = 20:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("/api/v1/tasks")
public Object findTasks(
// defaultValue macht den Parameter sozusagen immer vorhanden und gibt ein klares Verhalten vor
@RequestParam(defaultValue = "0") int page,
// Wenn size=mnogo gesendet wird, schlägt die Konvertierung in int fehl, bevor die Controller-Methode betreten wird
@RequestParam(defaultValue = "20") int size
) {
return taskService.findPage(page, size);
}
Hier gibt es sofort zwei nützliche Effekte.
Erstens macht defaultValue das Verhalten deterministisch: Wenn der Client den Parameter nicht sendet, wissen Sie genau, was passiert. Sie müssen nicht im Code überall „wenn null, dann 0“ verteilen.
Zweitens zwingt der Typ int (oder Integer) Spring, die Konvertierung durchzuführen. Wenn der Client Müll schickt, erreicht die Anfrage den Service nicht.
Manuell testen:
### First page, size 20
GET http://localhost:8080/api/v1/tasks?page=0&size=20
Accept: application/json
Und jetzt ein Gegenbeispiel:
### Wrong: page is not a number
GET http://localhost:8080/api/v1/tasks?page=kotik&size=20
Accept: application/json
In diesem Fall ist die Konvertierung "kotik" → int nicht möglich und Spring bricht die Anfrage ab, bevor Sie überhaupt taskService sagen können.
Primitive und Wrapper: int und Integer
Hier gibt es einen praktischen Aspekt. Wenn der Parameter optional sein kann, ist Integer manchmal ehrlicher, weil er null sein kann. Aber wenn Sie ohnehin Defaults möchten, ist defaultValue + int in der Regel die einfachste und sauberste Lösung.
| Argumentform | Kann fehlen? | Was passiert, wenn es fehlt | Wann sinnvoll |
|---|---|---|---|
| int page + defaultValue | ja | default wird eingesetzt | für page/size fast immer |
| Integer page + required=false | ja | wird null | wenn Sie explizit zwischen „nicht gesendet“ unterscheiden wollen |
| int page + required=false ohne default | formal ja, aber Sinn unklar | leichte falsche Erwartungen | besser vermeiden, kein Glücksspiel |
Wenn Sie zu Beginn des Kurses Ihr zukünftiges Ich so wenig wie möglich überraschen möchten, wählen Sie defaultValue für technische Zahlen. Das ist in der Regel einfacher zu lesen und zu testen.
5. Binding von Datumsangaben: LocalDate und ISO-Format
Daten in APIs sind eine ewige Schmerzquelle, weil Leute sie gerne so schreiben, wie sie es gewohnt sind: 21.03.2026, 03/21/2026, 21-03-26 und noch ein Dutzend Variationen „wie in unserer Buchhaltung“. Aber eine API ist ein Vertrag, kein freies Schreiben. Deshalb wählen wir ein Format und halten uns daran. In Spring MVC ist für ein Datum ohne Uhrzeit der fast ideale Typ LocalDate.
In der Task Tracker API hat eine Aufgabe eine Frist dueDate, und zum Filtern der Liste sind Parameter wie dueBefore und dueAfter sinnvoll. Dies ist ein Datum ohne Uhrzeit, daher ist LocalDate ideal: keine Zeitzone, kein „warum wurde aus gestern heute“.
Beispielsignatur:
import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("/api/v1/tasks")
public Object findTasks(
// Parameter optional: Wenn dueBefore nicht gesendet wird, ist es null
@RequestParam(required = false)
// Format explizit festlegen: Erwartet ISO-Datum YYYY-MM-DD (und nicht „wie gewohnt“)
@DateTimeFormat(iso = ISO.DATE) LocalDate dueBefore
) {
return taskService.findByDueBefore(dueBefore);
}
Hier ist @DateTimeFormat(iso = ISO.DATE) ein Weg, den Vertrag expliziter zu machen. Auch wenn standardmäßig bereits das ISO-Format verwendet wird, wirkt die Annotation wie ein Verkehrsschild: Der Leser des Codes und Sie selbst in einem Monat verstehen sofort, dass YYYY-MM-DD erwartet wird.
Testanfrage:
### Tasks due before a specific date
GET http://localhost:8080/api/v1/tasks?dueBefore=2026-03-21
Accept: application/json
Und jetzt das, was aus Gewohnheit oft gemacht wird und einen Konvertierungsfehler verursacht:
### Wrong: non-ISO date format
GET http://localhost:8080/api/v1/tasks?dueBefore=21.03.2026
Accept: application/json
Spring wird ehrlich sagen: „Kann das Datum nicht parsen“. Und das ist normal: Sie sind nicht verpflichtet, alle Formate der Welt zu unterstützen. Sie sind verpflichtet, das zu unterstützen, das Sie im Vertrag beschrieben haben.
Einstellungen spring.mvc.format.*
Weil das Format von Datum und Uhrzeit in Spring MVC tatsächlich mit den Formateinstellungen zusammenhängt. Das kann global über Eigenschaften wie spring.mvc.format.* in der application.yml festgelegt werden. Aber ein wichtiger Hinweis: Heute konfigurieren wir keine globale Konvertierung, wir verstehen nur, dass es sie gibt und dass man das Format nicht dem Zufall überlassen sollte.
Wenn Sie nur verstehen wollen, „wo das lebt“, reicht dieses mentale Beispiel:
spring:
mvc:
format:
date: yyyy-MM-dd
In einem Übungsprojekt ist es normalerweise vorteilhafter, sich an ISO-Formate zu halten, da sie gleichermaßen von Menschen und Maschinen gelesen werden und nicht von der Locale des Entwicklerrechners abhängen.
6. Konvertierungsfehler und Signatur
Jetzt möchte ich fragen: „Okay, und was passiert, wenn der Wert falsch ist?“ Und das ist eine gute Frage – aber wichtiger ist etwas anderes: wann das passiert. Der Konvertierungsfehler tritt vor Ihrer Geschäftslogik auf. Das heißt, Sie können einen Breakpoint im Service setzen, eine Anfrage senden, einen Fehler in der Antwort sehen – und der Breakpoint wird nicht ausgelöst. Nicht weil Spring kaputt ist, sondern weil es nicht verpflichtet ist, Ihre Methode mit ungültigen Typen aufzurufen.
Um das zu fühlen, ist es manchmal nützlich, nur für sich und vorübergehend, eine primitive Diagnostik zu machen: zum Beispiel etwas im Controller auszugeben. Das ist keine Best Practice für die Produktion, aber als didaktischer Marker okay.
import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("/api/v1/tasks")
public Object findTasks(
// Hier ist der Parameter erforderlich: Wenn kein Status gesendet wird, gibt es 400 noch bevor die Methode ausgeführt wird
@RequestParam TaskStatus status
) {
// Didaktische Diagnose: Sie sehen die Ausgabe nur, wenn das Binding erfolgreich war
System.out.println("Controller method invoked");
return taskService.findByStatus(status);
}
Wenn Sie status=TODO senden, erscheint die Zeile in der Konsole. Wenn Sie status=todo senden, wird die Methode nicht einmal ausgeführt.
Warum ist das gut für die API? Weil die Grenze klar wird. Der Client hat einen ungültigen Parameter gesendet – die Anfrage gilt nicht als korrekt und der Server tut nicht so, als hätte er verstanden. Das ist Vertragsdisziplin. Später im Kurs lernen wir, solche Situationen in ein einheitliches und schönes Fehlerformat zu verwandeln, aber schon jetzt ist es wichtig, die Mechanik zu sehen: Der Controller ist nicht der erste Punkt, an dem etwas schiefgehen kann.
Mini-Refactoring GET /api/v1/tasks
Wenn wir das Binding verstanden haben, möchten wir ein kleines Gerüst für den zukünftigen List-Endpunkt bauen. Ich betone: Wir machen noch keine vollständige Filterung, Sortierung und Paginierung; wir zeigen nur, dass die Controller-Signatur typisiert und verständlich sein kann. Das ist wie das Anbringen der richtigen Steckdosen in der Wohnung, bevor man die Geräte kauft.
Hier ist ein Beispiel für eine saubere Signatur, die bereits wie ein normaler Vertrag aussieht:
import java.time.LocalDate;
import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("/api/v1/tasks")
public Object findTasks(
// Enum: entweder null (kein Filter) oder einer der zulässigen TaskStatus-Werte
@RequestParam(required = false) TaskStatus status,
// Technische Paginierung: stabile Standardwerte
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
// Datumsangaben: ISO-Format festlegen und Fehlen des Parameters zulassen
@RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE) LocalDate dueBefore,
@RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE) LocalDate dueAfter
) {
return taskService.findTasks(status, page, size, dueBefore, dueAfter);
}
Und das ist das Schöne am Service: Er denkt nicht mehr über Strings nach. Er denkt über semantische Typen nach:
import java.time.LocalDate;
import com.example.tasktracker.domain.model.TaskStatus;
public interface TaskService {
// Der Service-Vertrag ist bereits typisiert: keine Strings aus HTTP – nur Domain-/technische Typen
Object findTasks(TaskStatus status, int page, int size, LocalDate dueBefore, LocalDate dueAfter);
}
Und das ist wohl das wichtigste praktische Fazit der heutigen Vorlesung: Ein guter API-Vertrag beginnt damit, dass der Datentyp bewusst gewählt wird und Eingabefehler an der Grenze auftreten, nicht im gesamten Inneren der Anwendung.
7. Typische Fehler beim Binding von Enums, Zahlen und Datumsangaben
In diesem Thema sind die Fehler meist nicht kompliziert, sondern hinterhältig: Sie haben fast alles richtig gemacht, aber ein Detail verwandelt die Anfrage noch vor Ihrem Code in eine 400. Nachfolgend die häufigsten Stolpersteine, auf die man in Spring MVC tritt, besonders am Anfang.
Fehler Nr. 1: Status als String akzeptieren und dann manuell im Controller parsen.
Solcher Code verwandelt den Controller schnell in einen Mini-Parser: if ("TODO".equals(status)) ... else .... Der Vertrag wird vage und Fehler treten nicht an der Grenze auf, sondern tief in der Anwendung. Wenn der Wert auf eine feste Menge begrenzt ist, macht das Enum TaskStatus diese Menge explizit und Spring prüft selbst die Korrektheit.
Fehler Nr. 2: Erwarten, dass der Enum „von selbst“ Werte wie in_progress oder InProgress versteht.
Standardmäßig stützt sich das Spring-Enum-Binding auf die Konstantennamen. Wenn der Vertrag IN_PROGRESS sagt, dann ist genau das zu senden. Der Wunsch, „alle Schreibweisen“ zu akzeptieren, wirkt freundlich, endet aber oft im Chaos: Die Kunden fangen an, alles Mögliche zu senden, und Sie unterstützen dann einen Zoo. Besser ein kanonisches Format und eine ordentliche Dokumentation.
Fehler Nr. 3: Verwenden eines numerischen Parameters ohne defaultValue und sich dann über das Verhalten bei fehlendem Wert wundern.
Bei technischen Parametern wie page und size ist es fast immer besser, klare Standards zu setzen. Wenn Sie @RequestParam(defaultValue="0") int page schreiben, ist das Verhalten vorhersagbar. Wenn Sie es „laufen lassen“, schaffen Sie selbst zukünftige Verwirrung: Ist 0 der „Standard“ oder wurde der Wert nicht gesendet?
Fehler Nr. 4: Daten im Format schreiben, wie es Menschen gewohnt sind, nicht wie Computer.
21.03.2026 sieht menschlich aus, aber für ein API-Format ist das kein Argument. In REST APIs gewinnt fast immer das ISO-Format YYYY-MM-DD. Wenn Sie LocalDate verwenden, geben Sie gleich Beispiele für Anfragen im ISO-Format und fixieren Sie dies ggf. über @DateTimeFormat(iso = ISO.DATE), damit nicht das Gefühl entsteht, „es hat sich irgendwie selbst erraten“.
Fehler Nr. 5: Versuchen, Konvertierungsfehler im Service zu fangen und „geschäftlich zu behandeln“.
Der Typkonvertierungsfehler tritt früher auf – bei der Vorbereitung der Argumente der Controllermethode. Daher wird der Service ihn nicht sehen, der Controller wird möglicherweise nicht einmal starten. Wenn Sie erwarten, „im Service zu prüfen, dass page eine Zahl ist“ – haben Sie schon verloren: Eine Anfrage mit page=kotik wird den Service nicht erreichen. Das ist normal; man muss sich nur merken, wo die Grenze verläuft.
GO TO FULL VERSION