1. Web Starter – einsatzbereite Laufzeitumgebung
An diesem Punkt ist bereits klar: Nach dem Einbinden von spring-boot-starter-webmvc startet die Anwendung nicht nur, sondern hält einen Port und antwortet per HTTP. Jetzt ist es wichtig, diese Tatsache nicht erneut zu beweisen, sondern die dahinterstehenden Laufzeitschichten zu verstehen.
Ein Controller ist nur die Spitze des Eisbergs. Damit eine Anfrage überhaupt Ihre Methode erreicht, benötigt die Anwendung einen eingebetteten Server, eine MVC-Schicht und einen Mechanismus, der den Rückgabewert in eine HTTP-Antwort umwandeln kann. In Boot wird all dies rund um SpringApplication.run(...) aufgebaut: Der Einstiegspunkt ändert sich nicht, sondern die Menge der Subsysteme, die Boot aus dem Classpath zusammenstellt.
2. Was steckt in spring-boot-starter-webmvc
Im Namen des Starters steckt das Wort starter, und das ist kein reines Marketing. Es ist buchstäblich ein „Starterset“ – kuratierte Abhängigkeiten, die über das Boot BOM aufeinander abgestimmt und so ausgewählt sind, dass sie zusammen ein funktionierendes Ergebnis liefern. Wenn Sie versuchen, dies manuell zusammenzustellen, werden Sie schnell die klassische Junior-Quest erleben: „Warum hier NoSuchMethodError“, „Warum nicht der Jackson“, „Warum startet der Server nicht“.
In build.gradle.kts reicht hier eine einzige Abhängigkeit, und für uns ist es jetzt nützlicher, nicht die Bibliotheksliste auswendig zu lernen, sondern die Schichten zu verstehen, die entstehen. Das kann man sich gut so vorstellen:
| Was wird zum Projekt hinzugefügt | Was bringt es zur Laufzeit | Einfache Erklärung |
|---|---|---|
| Eingebetteter Servlet-Container (meist Tomcat) | Die Anwendung beginnt, einen Port abzuhören und HTTP zu empfangen | „Ihr Service wird selbst zum Miniserver“ |
| Spring MVC | Routing von Anfragen zu Methoden, Arbeit mit Parametern und Antworten | „Das Gehirn, das versteht: Welche Methode soll aufgerufen werden“ |
| Konverter für Anforderungs-/Antwortkörper (HttpMessageConverter) | Umwandlung von String/Map/Objekten in den HTTP-Antwortkörper | „Übersetzer zwischen Java und HTTP“ |
| JSON-Stack (Jackson) | Automatische Serialisierung von Objekten in JSON | „Wir schreiben JSON nicht manuell als String“ |
Und jetzt ein wichtiger Punkt: Der Starter enthält nicht die gesamte Welt der Webentwicklung für Sie. Beispielsweise ist eine vollständige Validierung eingehender Daten nicht „zwingend“ im webmvc-Starter enthalten (in Boot wird dafür normalerweise ein separater Starter eingebunden). Und das ist in Ordnung: Wir bauen gerade eine Web-Basislinie auf, nicht „alles auf einmal“.
Wenn Sie sich vergewissern möchten, dass die Abhängigkeiten tatsächlich gezogen wurden, können Sie mit einem Auge auf den Abhängigkeitsbaum schauen (wir kürzen ihn auf das Wesentliche, um nicht zu ertrinken):
# Wir betrachten die Laufzeitabhängigkeiten, die in die Anwendung gelangen:
$ ./gradlew dependencies --configuration runtimeClasspath
# In der Ausgabe wird sichtbar, dass ein Starter Tomcat, MVC und den JSON-Stack mitbringt:
+--- org.springframework.boot:spring-boot-starter-webmvc
| +--- org.springframework.boot:spring-boot-starter-tomcat
| | \--- org.apache.tomcat.embed:tomcat-embed-core
| +--- org.springframework:spring-webmvc
| \--- org.springframework.boot:spring-boot-starter-json
| \--- tools.jackson.core:jackson-databind
Dies ist keine „heilige Liste“, sondern lediglich eine Bestätigung der Idee: Ein Starter bringt sowohl Server, MVC als auch die JSON-Basis mit.
3. Eingebetteter Container: Tomcat in der Anwendung
Wenn von „eingebettetem Container“ die Rede ist, haben Anfänger manchmal das seltsame Gefühl, dass in der Anwendung ein kleiner Gnom haust und manuell auf Anfragen antwortet. Die Realität ist langweiliger und besser: Boot startet einen gewöhnlichen Servlet-Container (standardmäßig oft Tomcat), aber als Teil Ihres Prozesses. Das heißt, der Server ist kein separates Programm, das installiert und mit einem „Deploy“ versehen werden muss, sondern eine Bibliothek innerhalb Ihrer Anwendung.
Dadurch ändert sich das mentale Modell des Starts. Früher sah „Web auf Java“ in alten Lehrbüchern so aus: WAR bauen → Tomcat separat installieren → deployen → beten. In der Boot-Welt ist das Basismodell ein anderes: Sie starten die Anwendung – und sie bringt den Server selbst zum Laufen. Das ist sehr praktisch zum Lernen und für kleine Dienste, weil es eine ganze Schicht „externer Infrastruktur“ zu Beginn aus Ihrem Kopf entfernt.
Die Logs sind hier nützlich, nicht zur erneuten Überprüfung, sondern weil man daran sieht, welcher Container genau gestartet ist und auf welchem Port er lauscht. Nach dem Einbinden des Web Starters sehen Sie etwa Folgendes:
# Diese Zeilen bedeuten, dass der Servlet-Container tatsächlich gestartet ist und den Port belegt:
... Tomcat initialized with port 8080
... Starting service [Tomcat]
... Starting Servlet engine: [Apache Tomcat/...]
... Tomcat started on port 8080 (http) with context path ''
... Started CatalogServiceApplication in 1.234 seconds
Zwei Gedanken, die man laut aussprechen sollte, damit sie „hängen bleiben“. Erstens: Der Port 8080 erscheint nicht, weil Sie ihn irgendwo im Code angegeben haben – es ist der Standard des Servers. Zweitens: Die Anwendung beendet sich nach dem Start nicht, weil es jetzt einen aktiven Serverzyklus gibt: Der Dienst lauscht auf dem Port und wartet auf eingehende Verbindungen.
Und ja, Tomcat ist keine Katze. Auch wenn er sich manchmal wie eine Katze verhält: Er taucht von selbst auf, belegt Speicher und tut so, als wäre das so beabsichtigt gewesen.
flowchart TD
Client[Browser / curl] --> Tomcat[Embedded server]
Tomcat --> MVC[Spring MVC runtime]
MVC --> Controller["Ihr @RestController"]
Controller --> Conv[HttpMessageConverter]
Conv --> Client
4. Spring MVC: Was automatisch hochfährt
Wenn der Server läuft, stellt sich die nächste logische Frage: „Okay, der Port wird abgehört. Aber wer versteht darin, dass /ping die Methode ping() aufrufen soll?“ Genau hier kommt Spring MVC ins Spiel – die Web-Framework-Schicht innerhalb des Spring Frameworks. Es ist wichtig zu verstehen: Spring MVC ist nicht eine einzelne Annotation und nicht eine einzelne Klasse, sondern eine Reihe von Komponenten, die als Beans im ApplicationContext leben und in der Lage sind, Webanfragen zu verarbeiten.
Deshalb haben wir in den letzten Tagen so sehr auf den Gedanken „Der Container ist das Zentrum des Anwendungslebens“ gedrängt. Wenn Sie den Web Starter hinzufügen, registriert Boot automatisch viele infrastrukturelle Beans, die die MVC-Engine bilden. Sie erstellen sie nicht manuell, aber sie sind genauso real wie Ihr CourseCatalogService.
Ein anschauliches (und bewusst vereinfachtes) Schema dessen, was entsteht, sieht so aus:
flowchart TD
Client[Browser / curl] --> Tomcat[Embedded server]
Tomcat --> MVC[Spring MVC runtime]
MVC --> Controller["Ihr @RestController"]
Controller --> Conv[HttpMessageConverter]
Conv --> Client
Für uns reicht hier etwas anderes: Nach dem Einbinden des Starters entsteht eine fertige Infrastruktur, in die Sie einfach Ihre Controller „einhängen“. Die Details des Routings und Bindings lassen wir vorerst beiseite – zuerst ist es wichtig, das MVC-Laufzeitgerüst selbst zu sehen.
Ein minimales Controller-Beispiel, um zu spüren, wie wenig Code von Ihrer Seite nötig ist:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // Der Rückgabewert der Methoden wird zum Body der HTTP-Antwort
class PingController {
@GetMapping("/ping") // Route: GET /ping
String ping() {
return "ok"; // Als Antwort geht ein String (in der Regel als text/plain)
}
}
Diese Klasse wird ein Bean (durch Component Scanning), MVC erkennt @GetMapping("/ping"), und wenn eine Anfrage GET /ping eintrifft, ruft das Framework ping() auf. Kein new PingController() und kein „Wo ist mein main für die Controller“.
5. HttpMessageConverter: Objekt in HTTP-Antwort
Bisher haben wir gesagt „Wir geben einen Wert aus der Methode zurück – und er geht in die Antwort“. Das klingt zu märchenhaft, also lassen Sie uns die Magie erden. Eine HTTP-Antwort ist letztlich Status, Header und ein Byte-Array (Body). Aber Ihre Controller-Methode gibt … String, Map oder ein CourseCard zurück. Es wird eine Brücke zwischen der „Welt von Java“ und der „Welt von HTTP“ benötigt. Diese Brücke heißt in Spring MVC HttpMessageConverter.
HttpMessageConverter ist eine Komponente, die Ihren Rückgabewert in den HTTP-Antwortkörper „schreiben“ kann (und den passenden Content-Type setzt). In die andere Richtung kann er auch arbeiten (den Anforderungskörper lesen und in ein Objekt umwandeln), aber heute konzentrieren wir uns auf die Antwort.
Es ist sehr nützlich, sich eine einfache Formel zu merken:
@RestController → Rückgabewert der Methode wird zum „Body“ der Antwort → HttpMessageConverter wählt das Format und schreibt die Bytes.
Für verschiedene Datentypen gibt es verschiedene Konverter. Eine beispielhafte Tabelle (nicht zum Auswendiglernen, sondern zum Verständnis):
| Was die Methode zurückgibt | Was normalerweise rauskommt | Warum |
|---|---|---|
| String | text/plain (normalerweise) | Es gibt einen Konverter „String → Text“ |
| Map<String, Object> | application/json | Es gibt einen JSON-Konverter auf Basis von Jackson |
| byte[] | application/octet-stream | Es gibt einen Konverter „Bytes wie sie sind“ |
Jetzt wird klar, warum der Web Starter so wichtig ist: Er bringt nicht nur MVC mit, sondern auch eine Reihe von Konvertern (einschließlich JSON über Jackson). Deshalb erhalten Sie Ihr „erstes JSON“ nicht durch manuelle String-Konstruktion, sondern durch den normalen Framework-Mechanismus.
Wenn wir beispielsweise ein kleines JSON ohne Erstellung einer separaten Klasse ausliefern möchten, können wir eine Map zurückgeben:
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // Dies ist ein API-Controller (keine Seiten)
class AppInfoController {
@GetMapping("/api/info") // Route: GET /api/info
Map<String, Object> info() {
// Map wird von Jackson automatisch in JSON serialisiert
return Map.of("service", "catalog-service", "status", "UP");
}
}
Und das ist bereits eine JSON-Antwort, weil MVC den JSON-Konverter auswählt und die Map in JSON serialisiert.
Hier versteckt sich auch eine typische Falle: Wenn Sie einen String zurückgeben, bedeutet das nicht, dass Sie JSON zurückgeben. Es bedeutet, dass Sie einen String zurückgeben. Und in den meisten Fällen erhält der Client text/plain. Wenn Sie eine „strukturierte Antwort“ wünschen, geben Sie ein Objekt/Map zurück, keinen String mit geschweiften Klammern.
6. Content-Type und JSON in der Praxis
Bis Sie dies mit eigenen Augen sehen, könnte Ihr Gehirn weiter zweifeln: „Ja, vielleicht macht es da irgendwas von selbst.“ Der einfachste Weg, den Zweifel zu beseitigen, ist ein Blick auf die Antwort-Header. Dafür eignet sich curl -i hervorragend, da es sowohl Header als auch Body anzeigt.
Wenn wir einen PingController haben, dann:
$ curl -i http://localhost:8080/ping
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
ok
Wenn Sie dagegen den AppInfoController aufrufen, sehen Sie einen anderen Content-Type:
$ curl -i http://localhost:8080/api/info
HTTP/1.1 200
Content-Type: application/json
{"service":"catalog-service","status":"UP"}
Genau das ist die Arbeit von HttpMessageConverter: Derselbe MVC-Mechanismus, aber unterschiedliche Konverter für verschiedene Ergebnistypen. Sie haben weder toJson() noch new ObjectMapper() noch String-Konkatenation geschrieben. Sie haben einfach eine normale Java-Struktur zurückgegeben – und MVC hat seine Arbeit gemacht.
Und jetzt ein wichtiger konzeptioneller Gedanke für unseren catalog-service. Wir haben bereits ein Domänenmodell (CourseCard, Money, Enums CourseTrack, CourseLevel) und eine Serviceschicht, die Daten als Java-Objekte ausliefert. Nach dem Einbinden des Web Starters und mit dem JSON-Konverter können wir die Web-Schicht maximal dünn halten: Der Controller nimmt das Ergebnis aus CourseCatalogService und gibt es nach außen weiter, ohne sich um die Serialisierung zu kümmern.
Wenn Ihr CourseCard beispielsweise als Record implementiert ist (ein normaler moderner Stil), kann Jackson es „out of the box“ serialisieren:
import java.time.LocalDate;
public record CourseCard(
String id, // Kursidentifikator
String title, // Kurstitel
LocalDate launchDate // Startdatum (Jackson kann mit Datumsangaben umgehen, über die Standardmodule von Boot)
) {}
An dieser Stelle möchten Anfänger oft „schön und zuverlässig“ mit manuellem JSON arbeiten. Aber in einem Boot-Projekt ist das in der Regel genau das Gegenteil: Manuelles JSON ist ein Weg, es schlechter zu machen. Es bricht an Anführungszeichen-Escaping, an Datumsangaben, an Null-Werten und an jeder Weiterentwicklung des Modells. Der eingebaute Mechanismus ist genau dafür da, dass Sie dies vermeiden.
7. Typische Fehler bei der Arbeit mit dem Web Starter
Fehler Nr. 1: Erwarten, dass der Web Starter „einfach nur Annotationen“ ist.
Ein Neuling bindet spring-boot-starter-webmvc ein, schreibt @GetMapping, denkt aber trotzdem, dass man irgendwo „den Server manuell starten“ oder „einen DispatcherServlet von Hand erstellen“ müsste. Im Boot-Modell sind Server und MVC-Infrastruktur Teil der Auto-Configuration. Wenn der Starter eingebunden ist, liegt das Problem meistens nicht darin, dass „der Server nicht gestartet wurde“, sondern dass der Controller nicht im Component Scan ist oder die Anwendung wegen eines Fehlers nicht gestartet ist.
Fehler Nr. 2: Verwechseln von @Controller und @RestController und sich dann wundern, dass „der String zum Seitenname wird“.
Wenn Sie @Controller (ohne @ResponseBody) verwenden, wird der zurückgegebene String als View-Name (Seitenname) interpretiert. Bei @RestController (oder @Controller + @ResponseBody) wird der String zum Antwortkörper. Zu Beginn des Kurses ist es einfacher, bei @RestController zu bleiben, um Webseiten und JSON-Antworten nicht in einem Kopf zu vermischen.
Fehler Nr. 3: JSON manuell durch String-Konkatenation zusammenbauen.
Klassiker: return "{\"status\":\"UP\"}";. Das sieht „schnell“ aus, aber Sie verlieren den Content-Type, machen leicht Fehler bei den Anführungszeichen und leiden später bei Datumsangaben, Zahlen und verschachtelten Objekten. In MVC gibt es einen normalen Mechanismus: Geben Sie eine Map oder ein Java-Objekt zurück, und lassen Sie HttpMessageConverter + Jackson das tun, wofür sie überhaupt existieren.
Fehler Nr. 4: In Panik wegen Port 8080 geraten und versuchen, das „mit Code“ zu beheben.
Wenn Sie beim Start einen Fehler sehen, dass der Port belegt ist, ist die erste Reaktion „Spring ist kaputt“. In Wirklichkeit läuft meistens bereits eine andere Anwendung auf 8080 (eine zweite Instanz des Dienstes, Docker, ein alter Tomcat, was auch immer). Auf dem Niveau des heutigen Themas reicht es zu verstehen: Der eingebettete Server versucht, den Port zu belegen. Wenn der Port belegt ist, startet der Server ehrlicherweise nicht. Das ist kein Bug von MVC und kein Problem der Controller.
Fehler Nr. 5: Nicht auf den Content-Type achten, sondern Schlussfolgerungen „nach Gefühl“ ziehen.
Manchmal sagt ein Student: „Ich habe eine Map zurückgegeben, aber ich bekomme kein JSON.“ Und dann stellt sich heraus, dass er im Browser die „schöne Darstellung“ oder ein Log ansieht, nicht die tatsächlichen HTTP-Header. Die Gewohnheit, curl -i zu verwenden, macht aus Chaos schnell Klarheit: Sie sehen Status, Content-Type und den Antwortkörper in reiner Form.
GO TO FULL VERSION