CodeGym /Kurse /Spring REST & MVC /Endpunkte in Spring MVC

Endpunkte in Spring MVC

Spring REST & MVC
Level 5, Lektion 1
Verfügbar

1. Endpunkt in Spring MVC als Teil des Vertrags

Wenn ein Anfänger zum ersten Mal einen Spring-Controller sieht, entsteht leicht das Gefühl, dass Annotations nur ein Weg sind, „eine URL irgendwie an eine Methode zu hängen". Aber in Wirklichkeit tust du etwas Wichtigeres: Du beschreibst das öffentliche Interface deines Services. Ein Endpunkt ist nicht einfach eine Java-Methode, sondern die Verbindung „HTTP-Methode + Pfad", und genau diese wird lange und stabil leben, auch wenn du die Hälfte des internen Codes umschreibst.

Um den Unterschied zu spüren, stell dir vor, eine Java-Methode ist ein „innerer Knopf" in deinem Gerät, und ein Endpunkt ist der Knopf auf dem Panel außen, auf den Clients tatsächlich drücken. Dem Client ist es egal, wie deine Methode listTasks() heißt — er wird sie nie zu Gesicht bekommen. Dem Client ist wichtig, dass er GET /api/v1/tasks aufrufen und eine verständliche Antwort bekommen kann.

In diesem Kurs behandeln wir Endpunkte bewusst als Vertrag, deshalb interessieren uns zwei Dinge: wie man einen Endpunkt im Code deklariert und wie man das lesbar macht. Lesbar bedeutet, dass du eine Klasse öffnest, über die Annotations drüberschaust und bereits verstehst, welche Routen unterstützt werden. Fast wie eine U-Bahn-Karte: Linien und Stationen müssen sichtbar sein, sonst werden Fahrten zum Rätsel.

Eine kleine „Vertrags-Tabelle", damit man einen Anhaltspunkt im Kopf hat:

Vertragsteil Beispiel Wie wir es in Spring MVC ausdrücken
HTTP-Methode GET @GetMapping (oder
@RequestMapping(method=GET)
)
Pfad /api/v1/tasks @RequestMapping("/api/v1/tasks"),
@GetMapping("/summary")
usw.
Wo der Endpunkt lebt Controller + Methode @RestController + Methode mit Mapping-Annotation

Über Request-Body, JSON-Serialisierung und Antwort-Statuscodes reden wir jetzt bewusst nicht. In dieser Vorlesung ist es wichtiger zu lernen, Routen zu deklarieren, sodass man am Controller-Code das API-Design ablesen kann.

2. @RestController: API-Modus

Wenn du ein Backend-API schreibst, möchtest du meistens, dass die Controller-Methode Daten zurückgibt, die zum Body der HTTP-Antwort werden. Hier entsteht eine Verwirrung: In Spring gibt es „Controller für Seiten" (wenn du einen HTML-Template-Namen zurückgibst) und „Controller für APIs" (wenn du Daten zurückgibst). @RestController sagt Spring genau das: „Das ist ein API-Controller, versuch nicht, einen HTML-Generator daraus zu machen."

Intern ist das in Spring recht elegant gelöst: @RestController ist im Grunde eine „zusammengesetzte Annotation", die bedeutet „Controller + Rückgabewert = Antwort-Body". Du musst dir die Implementierungsdetails nicht merken, es reicht, den Gedanken zu behalten: Wenn du eine API baust, die vor allem JSON liefert, dann ist die Basis-Annotation des Controllers — @RestController.

Der kleinste „Ping", der zeigt, dass ein Endpunkt deklariert ist und funktioniert, sieht so aus:

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

@RestController // Wir sagen Spring: Das ist ein REST-API-Controller, Rückgabewert = Antwort-Body
class PingController {

    @GetMapping("/api/v1/ping") // Vertrag: GET /api/v1/ping
    public String ping() {
        return "pong"; // Das wird zum Body der HTTP-Antwort
    }
}

Wenn ein Request GET /api/v1/ping an deine Anwendung kommt, ordnet Spring ihn dieser Methode zu und ruft ping() auf. Der String "pong" landet im Body der HTTP-Antwort (ja, einfach als String — das ist zur Demonstration normal, auch wenn in einer echten API meistens JSON kommt).

Ein praktischer Fallstrick, über den Anfänger sehr oft stolpern: Wenn du versehentlich @Controller statt @RestController setzt, kann Spring den String "pong" als View-Name interpretieren, also als „Name eines HTML-Templates." Und du sitzt da und denkst: „Warum gebe ich pong zurück, aber der Server sucht nach irgendeiner pong.html?" Das ist eine klassische Situation, wo es scheint, „Spring ist kaputt", obwohl du einfach einen anderen Betriebsmodus aktiviert hast.

Noch ein wichtiger Punkt: Spring muss deinen Controller finden. Das passiert normalerweise durch Component-Scanning: Wenn dein @SpringBootApplication im Root-Package com.example.tasktracker liegt, sieht Spring Klassen weiter unten im Package-Baum, z.B. com.example.tasktracker.api.controller. Deshalb lohnt es sich, Controller von Anfang an an einem klaren Ort zu halten, statt sie über das Projekt zu verteilen „wie Dateien auf dem Desktop."

3. @RequestMapping auf der Klasse: gemeinsames Präfix

Wenn du die Ressource tasks entwirfst, ist es logisch, dass alle Operationen darauf mit dem gemeinsamen Präfix /api/v1/tasks beginnen. Wenn du dieses Präfix in jede Methode des Controllers schreibst, entstehen viele Wiederholungen, und Wiederholungen sind fast immer zukünftige Fehler. Einmal vertippt, einmal /api/v1 vergessen, und plötzlich ist die Hälfte der API auf einen anderen Pfad „gewandert."

Genau dafür gibt es @RequestMapping auf Klassenebene: Es setzt das gemeinsame Pfad-Präfix für alle Methoden innerhalb des Controllers. Ein sehr praktisches mentales Modell: Die Controller-Klasse ist wie ein „Ordner", und die Methoden sind wie „Dateien" darin. Der Ordner gibt den Basispfad vor, und jede Methode fügt einen „Schwanz" hinzu.

Für die Mechanik des gemeinsamen Präfixes nehmen wir einen Demo-DemoTaskController: Hier wollen wir genau den Pfad auf Klassenebene sehen, ohne das gesamte Projekt-Drumherum.

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

@RestController // REST-Modus: Rückgabewerte der Methoden = Antwort-Body
@RequestMapping("/api/v1/tasks") // Basispräfix für alle Endpunkte dieses Controllers
class DemoTaskController {

    @GetMapping // GET auf den Basispfad des Controllers: /api/v1/tasks
    public String listTasks() {
        return "tasks"; // Zur Demonstration geben wir einen String zurück
    }
}

Hier sind zwei Dinge wichtig zu sehen.

Erstens: @RequestMapping("/api/v1/tasks") auf der Klasse sagt, dass der Controller insgesamt für die Ressource tasks in der Version /api/v1 zuständig ist.

Zweitens: @GetMapping ohne Argumente bedeutet „GET auf den Basispfad des Controllers". Der resultierende Endpunkt ist also GET /api/v1/tasks. Wir schreiben "/api/v1/tasks" kein zweites Mal, und das ist keine Faulheit, sondern Disziplin: Je weniger sich wiederholender Text im Vertrag, desto geringer die Chance, /tasks/tasks zu erzeugen.

Übrigens hat @RequestMapping mehrere Schreibweisen, die manchmal verwirren, aber inhaltlich nah beieinander sind:

// Alle drei Schreibweisen unten sind äquivalent: derselbe Pfad, nur verschiedene Parameternamen
@RequestMapping("/api/v1/tasks")
@RequestMapping(path = "/api/v1/tasks")
@RequestMapping(value = "/api/v1/tasks")

Im Lernprojekt wählst du am besten einen Stil und hältst dich daran. Normalerweise ist der einfachste am besten: @RequestMapping("/api/v1/tasks").

4. @GetMapping: lesbare GET-Deklaration

@RequestMapping ist ein „Universalschweizer Messer": Damit kann man Endpunkte für fast alles deklarieren. Aber das Problem mit Schweizer Messern ist, dass wenn man ständig nur Messer, Löffel und Gabel benutzt, es etwas laut wirkt, jedes Mal das „Universal-Tool mit 27 Funktionen" herauszuholen.

In Spring MVC gibt es eine ganze Familie spezialisierter Mapping-Annotations, die den Code kürzer und klarer machen. Für GET-Operationen verwendet man @GetMapping. Das ist kein „anderer Mechanismus", sondern nur eine besser lesbare Art zu sagen: „Diese Methode verarbeitet GET."

Vergleiche zwei Varianten. Beide funktionieren gleich:

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

@RestController // REST-API-Controller
class PingController {

    @GetMapping("/api/v1/ping") // Kurze und lesbare Vertragsdeklaration: GET /api/v1/ping
    public String ping() {
        return "pong"; // Geht in den Antwort-Body
    }
}

und die „universellere", aber lautere Variante:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController // REST-API-Controller
class PingController {

    @RequestMapping(path = "/api/v1/ping", method = RequestMethod.GET) // Dasselbe, aber über das universelle @RequestMapping
    public String ping() {
        return "pong"; // Antwort-Body
    }
}

In der zweiten Variante verbrauchen wir mehr Zeichen, um dasselbe zu sagen. Und in Vertrags-Code, der öfter gelesen als geschrieben wird, sind überflüssige Zeichen kein Schmuck, sondern Rauschen (manchmal sehr teures Rauschen).

Damit das Bild vollständig ist, hier eine kleine „Familien-Tabelle":

HTTP-Methode Mapping-Annotation Intuitiver Sinn (ohne Vertiefung)
GET @GetMapping Ressource/Collection lesen
POST @PostMapping Erstellen
PUT @PutMapping Vollständiger Ersatz
PATCH @PatchMapping Partielle Änderung
DELETE @DeleteMapping Löschen

Im Rahmen des heutigen Tages interessiert uns vor allem @GetMapping, weil wir mit einfachen Lesevorgängen starten. Aber es ist wichtig, sich jetzt schon an den Gedanken zu gewöhnen: In Spring MVC ist es üblich, den Web-Vertrag deklarativ auszudrücken — und das bedeutet so lesbar wie möglich.

5. Den vollständigen Endpunkt-Pfad zusammenbauen

Wenn du @RequestMapping auf der Klasse und eine Mapping-Annotation auf der Methode verwendest, ergibt sich der resultierende Pfad durch Zusammensetzen dieser zwei Teile. Das ist sehr praktisch, hat aber einen Nebeneffekt: Wenn du verwechselst, was auf der Klasse und was auf der Methode steht, baust du leicht einen Pfad zusammen, der formal valide, aber logisch sinnlos ist.

Lass uns diese Mechanik in einer Tabelle festhalten. Sagen wir, wir haben:

@RequestMapping("/api/v1/tasks")
class DemoTaskController { ... }

Dann gilt:

Annotation auf der Methode Resultierender Pfad Wie man das liest
@GetMapping /api/v1/tasks „Aufgabenliste" (Collection)
@GetMapping("/summary") /api/v1/tasks/summary „Aufgabenliste in einer speziellen Darstellung"
@GetMapping("/{taskId}") /api/v1/tasks/{taskId} „Eine Aufgabe nach ID" (Platzhalter im Pfad)

Beachte: In den Zeilen mit Methoden-Annotations fügen wir nur den „Schwanz" hinzu. Deshalb "/summary", nicht "/api/v1/tasks/summary".

Jetzt zeigen wir das Anti-Pattern, das buchstäblich bei jedem zweiten Anfänger vorkommt (kein Problem — das ist fast eine obligatorische Reifungsphase):

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

@RestController // REST-API-Controller
@RequestMapping("/api/v1/tasks") // Basispfad der Ressource tasks
class BadDemoTaskController {

    @GetMapping("/tasks") // Fehler: Segment wird wiederholt, ergibt /api/v1/tasks/tasks
    public String listTasks() {
        return "wrong"; // Demonstration: antwortet, aber der Vertrag wird weniger sauber
    }
}

Das Ergebnis ist der Pfad /api/v1/tasks/tasks. Technisch ist der Server nicht „kaputt" — er startet brav und verarbeitet diese URL. Aber der API-Vertrag wurde schlechter: Er ist weniger vorhersehbar, weniger lesbar und wird beim Client garantiert die Frage aufwerfen: „Warum kommt tasks zweimal vor?"

Manchmal denkt man: „Na und, der Client kann es ja aufrufen." Kann er. Aber genau solche Kleinigkeiten summieren sich und verwandeln eine API in eine Sammlung von Zufälligkeiten. Und wir lernen gerade, eine API so zu bauen, dass man sie ohne Gedankenlesen verstehen kann.

Wenn du es visualisieren möchtest, kannst du dir folgendes Schema vorstellen:

flowchart TB
    A["@RequestMapping('/api/v1/tasks') DemoTaskController"] --> B["@GetMapping listTasks()"]
    A --> C["@GetMapping('/summary') summary()"]
    A --> D["@GetMapping('/{taskId}') details()"]

Wichtig ist hier nicht, dass du Mermaid gelernt hast. Wichtig ist, dass du siehst: Die Klasse gibt die „Baumwurzel" vor, und die Methoden sind die Äste.

6. Zwei Stile der Pfaddeklaration

In Spring MVC begegnet man beiden Stilen, und beide sind technisch zulässig. Aber aus Sicht der Vertraglesbarkeit (und das ist unsere Hauptwährung in diesem Kurs) unterscheiden sie sich sehr.

Der erste Stil — „vollständiger Pfad in jeder Methode". Er sieht so aus:

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

@RestController // REST-API-Controller
class DemoTaskController {

    @GetMapping("/api/v1/tasks") // Vollständiger Pfad genau hier deklariert: GET /api/v1/tasks
    public String listTasks() {
        return "tasks"; // Vereinfachte Antwort zur Demonstration
    }
}

Der Vorteil: Der Pfad ist direkt in der Methode vollständig sichtbar. Der Nachteil: Wenn du 5–10 Methoden hast, bekommst du 5–10 Wiederholungen von /api/v1/tasks, und all diese Strings müssen im gleichen Zustand gehalten werden.

Der zweite Stil — „gemeinsames Präfix auf der Klasse". Er sieht so aus:

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

@RestController // REST-API-Controller
@RequestMapping("/api/v1/tasks") // Gemeinsames Präfix für alle Methoden
class DemoTaskController {

    @GetMapping // Hier sieht man die Operation, der Pfad wird automatisch zusammengesetzt: GET /api/v1/tasks
    public String listTasks() {
        return "tasks"; // Vereinfachte Antwort zur Demonstration
    }
}

Diese Variante ist normalerweise leichter zu pflegen und leichter als API-Karte zu lesen. Du siehst einmal, dass die Klasse über tasks ist, und musst nur noch die Operationen innerhalb der Ressource unterscheiden.

Im Lernprojekt Task Tracker API halten wir uns an den zweiten Stil, weil er das ressourcenbasierte Denken besser unterstützt: Ein Controller — eine Ressource, und der Ressourcenpfad ist auf der Klasse gesetzt.

Noch ein praktischer Punkt: @RequestMapping kann man sowohl auf Klassen als auch auf Methoden setzen. Aber „im Durchschnitt" ist die folgende Gewohnheit sehr gut: Auf der Klasse — @RequestMapping als Basispfad der Ressource, auf den Methoden — spezifischere Annotations (@GetMapping und andere). Das verringert die Chance, versehentlich einen Endpunkt zu erstellen, der auf „alle Methoden gleichzeitig" reagiert, ohne dass du es bemerkt hast.

7. Lesbarer Vertrag: Namen und URI-Struktur

Wenn du im Team arbeitest, werden Controller öfter gelesen als geschrieben. Kollegen lesen sie, wenn sie Features hinzufügen. Tester lesen sie (manchmal buchstäblich mit den Augen). Dein zukünftiges Ich liest sie, wenn du in zwei Monaten vergessen hast, was du genau gemeint hast. Deshalb muss ein Controller wie ein ordentliches Schild aussehen, nicht wie eine Leinwand à la „wird schon passen."

Es beginnt mit einer Einfachheit: Der Controller wird nach der Ressource im Singular benannt, z.B. TaskController, und der Ressourcenpfad im Plural, z.B. /tasks. Das wirkt wie eine Kleinigkeit, aber genau aus solchen Kleinigkeiten baut sich eine API auf, die man ohne Dokumentation erraten kann.

Dann kommt die Gruppierung. Man sollte nicht in einem Controller „alles rund um Aufgaben, Kommentare, Tags und das Wetter in Tokio" zusammenpacken. Auch wenn man es kann, auch wenn man sich dabei langweilt. Ein Controller — ein Verantwortungsbereich. Für den heutigen Tag reicht uns die Ressource tasks.

Beispiel eines Controllers, der sich wie eine kleine API-Karte liest:

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

@RestController // REST-API-Controller
@RequestMapping("/api/v1/tasks") // Ressource: tasks (Collection)
class DemoTaskController {

    @GetMapping // GET /api/v1/tasks — Liste abrufen
    public String listTasks() {
        return "list"; // Demo-Antwort
    }

    @GetMapping("/summary") // GET /api/v1/tasks/summary — alternative Darstellung
    public String summary() {
        return "summary"; // Demo-Antwort
    }
}

Hier werden noch Strings zurückgegeben, weil unser Ziel ist, die Struktur der Endpunkte zu sehen. Aber schon jetzt ist erkennbar, welche Routen verfügbar sind. Und das ist der Haupterfolgskriterium für den aktuellen Schritt: Du öffnest den Controller und verstehst, was er tut, ohne das „Innenleben" der Methoden zu lesen.

Und noch eine Mikrogewohnheit, die sehr hilft: Methodennamen nicht zu „technisch" machen. listTasks() ist besser als getMappingForTasks() (ja, sowas kommt vor). Ein Controller ist eine Schicht über dem Vertrag, und der Vertrag soll in Worten „was wir tun" sprechen, nicht „wie es technisch aufgebaut ist."

8. Typische Fehler bei der Endpunkt-Deklaration

Fehler Nr. 1: @Controller statt @RestController verwenden und sich wundern, „wohin der Antwort-Body verschwunden ist".
Wenn du eine API baust, die vor allem JSON liefert, und einen String oder ein Objekt zurückgibst, erwartest du, dass das der Antwort-Body wird. Aber @Controller ist historisch eher für MVC-Seiten, und der zurückgegebene String kann als View-Name interpretiert werden. Das Ergebnis: Du bekommst nicht, was du erwartest, und es scheint, „Spring spielt verrückt." Die Lösung ist einfach: Für REST-APIs verwendest du @RestController.

Fehler Nr. 2: Pfad-Segmente duplizieren und versehentlich /api/v1/tasks/tasks erhalten.
Das passiert, wenn auf der Klasse schon @RequestMapping("/api/v1/tasks") steht und du auf der Methode @GetMapping("/tasks") hinzufügst. Formal funktioniert alles, aber der Vertrag wird weniger sauber und weniger vorhersehbar. Eine gute Regel: Auf der Klasse der Basispfad der Ressource, auf der Methode nur den Schwanz hinzufügen ("/summary"), oder für die Ressourcenwurzel gar nichts hinzufügen.

Fehler Nr. 3: Verschiedene Ressourcen in einem Controller mischen „weil es so schneller geht".
Heute fügst du in den TaskController noch GET /api/v1/tags hinzu, „weil das ja auch irgendwie dazu gehört." Morgen kommen dort Kommentare dazu. Übermorgen ist der Controller ein Sammelsurium aus unzusammenhängenden Endpunkten. Der Preis für „jetzt schneller" ist „immer schmerzhafter." Ein Controller soll um eine Ressource gruppiert sein, sonst wird das Lesen des Vertrags schwer.

Fehler Nr. 4: Verbale Pfade wie /api/v1/getTasks oder /api/v1/tasks/listAll machen.
Wir haben uns bereits darauf geeinigt, in Ressourcen zu denken: tasks ist eine Collection. Zum Lesen der Collection haben wir GET. Wenn du ein Verb in den Pfad hinzufügst, verschlechterst du die Lesbarkeit und rutschst Richtung RPC-Stil. Pfad ist für die Ressource, Methode für die Operation.

Fehler Nr. 5: Ein zu allgemeines Mapping deklarieren und unbewusst „alles abfangen".
Ein Anfänger setzt manchmal @RequestMapping("/api/v1/tasks") auf eine Methode und vergisst, die HTTP-Methode einzuschränken — dann wundert er sich, dass der Endpunkt nicht so reagiert wie erwartet. Das universelle @RequestMapping erfordert Sorgfalt: Wenn du GET willst — verwende @GetMapping. Das ist sowohl klarer als auch sicherer für den Vertrag.

1
Aufgabe
Spring REST & MVC, Level 5, Lektion 1
Gesperrt
Gemeinsames Präfix der Ressource books
Gemeinsames Präfix der Ressource books
1
Aufgabe
Spring REST & MVC, Level 5, Lektion 1
Gesperrt
Zwei Ressourcen – zwei Controller
Zwei Ressourcen – zwei Controller
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION