Thymeleaf ins Backend einbinden: Controller, Model und das erste Template für den Flurmonitor
https://bildung.social/@oerinformatik/
https://oer-informatik.de/flurmonitor-thymeleaf-einbinden
Thymeleaf, Spring MVC, Model-View-Controller, Controller, Model, Template, ViewResolver, statische Ressourcen, Spring Security, Smoke-Test, Sprint Planning, Flurmonitor
tl/dr; (ca. 27 min Lesezeit): Sprint 1 beginnt: Die Developers zerlegen die erste Story des Flurmonitors in Aufgaben und binden Thymeleaf ins Spring-Boot-Backend ein. Ein
@Controllernimmt die Anfrage an, legt Daten insModelund gibt den Namen eines Templates zurück; Thymeleaf findet das Template unterclasspath:/templates/und rendert daraus HTML. Die statische Abfahrtsanzeige wird zum ersten Template, Stylesheet und Logo werden statische Ressourcen, Spring Security gibt die Seite ohne Anmeldung frei, und ein Smoke-Test prüft, dass das alles zusammen funktioniert [AI-Assisted]. (Zuletzt geändert am 11.11.2026)
Dieser Artikel ist Teil einer Artikelreihe über den Flurmonitor mit Thymeleaf: Projektübersicht / Scrum-Projektstart / Thymeleaf einbinden / Templates / Ansichtsmodell / Integrationstest der Web-Schicht / Konfiguration und Formatierung / Aktualisierung / Hinweise mit Formularen / Robustheit und Betrieb / GUI-Test mit Selenium / Akzeptanztest / Change Request Keycloak
Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.
Dieser Artikel baut auf der Kann-Liste der Artikel Sprint 0 für den Flurmonitor, Bootstrap-Grundlagen und Change Request: Anmeldung mit Spring Security nachrüsten auf.
Als Thymeleaf-Einbindungs-Grundlagen kann ich nach Durchlesen des Artikels…
… beschreiben, welche Aufgaben Model, View und Controller in einer Spring-Web-Anwendung übernehmen und welchen Weg eine Anfrage vom Browser bis zum fertigen HTML nimmt.
… einen
@Controller, der einen View-Namen zurückgibt, von einem@RestControllerunterscheiden, der Daten als JSON liefert.… in einer Handler-Methode Daten mit
model.addAttributean das Template übergeben und im Template mitth:textausgeben.… erklären, wie Spring Boot aus einem View-Namen über Präfix
classpath:/templates/und Endung.htmldie Template-Datei findet.… Stylesheets und Bilder als statische Ressourcen unter
src/main/resources/static/ablegen und ihre URL angeben.
Darüber hinaus kann ich als Thymeleaf-Einbindungs-Expertenwissen…
… eine bestehende
SecurityFilterChainso ergänzen, dass öffentliche Seiten und ihre Ressourcen ohne Anmeldung erreichbar sind, alles andere aber geschützt bleibt.… einen Smoke-Test schreiben, der mit dem ganzen Anwendungskontext prüft, dass eine Seite ausgeliefert wird.
… die Symptome 404, 401 und „Template nicht gefunden“ ihren typischen Ursachen zuordnen.
Im Scrum-Projektstart hat das Team das Product Backlog geschrieben und für Sprint 1 ein Ziel vereinbart: „Ein erster sichtbarer Monitor: Im Foyer erscheinen die offenen Fahrten, ohne Namen und aus 5 m lesbar.“ Jetzt entsteht die erste Zeile Code. Das Backend des Mitfahrlabors kann bisher nur JSON liefern. Am Ende dieses Artikels liefert es unter /flurmonitor zusätzlich eine HTML-Seite, die der Server selbst erzeugt.
Das Bild zeigt den Weg, den eine Anfrage dann durch das Backend nimmt. Jede Station bekommt in diesem Artikel einen eigenen Abschnitt:
Stand im Projekt: Sprint Planning für Sprint 1
Sprint-Ziel und Auswahl der Einträge stehen seit dem Ende von Sprint 0 fest: MON-01 bis MON-04 mit zusammen 13 Story Points. Im dritten Teil des Sprint Planning klären die Developers, wie sie die Arbeit erledigen. Dazu zerlegen sie die erste Aufgabe von MON-01 („Thymeleaf einbinden, FlurmonitorController mit erstem Template, Seite ohne Anmeldung freigeben“) in Schritte, die jeweils an einem Tag fertig werden. Diese Schritte sind keine neuen Backlog-Einträge, sondern der Plan der Developers; sie stehen im Sprint Backlog unter MON-01:
| Nr. | Aufgabe im Sprint Backlog | fertig, wenn … | Abschnitt |
|---|---|---|---|
| T1 | Thymeleaf als Abhängigkeit einbinden | Gradle- und Maven-Build laufen durch | Thymeleaf einbinden |
| T2 | FlurmonitorController mit Model anlegen |
GET /flurmonitor gibt den View-Namen flurmonitor zurück |
Controller |
| T3 | statische Abfahrtsanzeige als Template übernehmen | Template liegt unter templates/, zeigt Werte aus dem Model |
erstes Template |
| T4 | Stylesheet und Logo als statische Ressourcen ablegen | Seite erscheint im Browser mit Farben und Logo | statische Ressourcen |
| T5 | Seite und Ressourcen ohne Anmeldung freigeben | Monitor im Foyer braucht kein Passwort, die API bleibt geschützt | Freigabe |
| T6 | Smoke-Test schreiben, Pipeline grün | Test läuft in test, Abdeckung bleibt über dem Gate |
Smoke-Test |
Wer welche Aufgabe übernimmt, legt niemand von außen fest: Die Developers verteilen sie selbst, meist im Daily Scrum. T1 bis T3 bauen aufeinander auf, T4 und T5 können parallel laufen. MON-01 selbst ist nach diesen sechs Aufgaben noch nicht fertig: Die Abnahmekriterien verlangen die echten Fahrten ohne Namen, und die liefern erst die nächsten Artikel. Fertig ist aber ein erstes, prüfbares Stück, das schon die Definition of Done erfüllen muss: Tests grün, Pipeline grün, Review im Merge Request.
Model, View und Controller in Spring MVC
Bisher hat das Backend nur REST-Schnittstellen angeboten: Ein Client wie die Web-App fragt Daten an, bekommt JSON und baut die Oberfläche selbst. Beim Flurmonitor ist es umgekehrt. Der Browser im Foyer soll keine eigene Logik ausführen, sondern fertiges HTML anzeigen. Das HTML entsteht deshalb auf dem Server; man spricht von serverseitigem Rendern (Server-Side Rendering, SSR).
Spring organisiert das nach dem Muster Model-View-Controller (MVC). Es trennt drei Aufgaben, die in einer Webseite sonst leicht durcheinandergeraten:
| Bestandteil | Aufgabe | im Flurmonitor |
|---|---|---|
| Controller | nimmt die Anfrage an, ruft die Fachlogik auf, entscheidet, welche Seite angezeigt wird | FlurmonitorController mit der Methode anzeigen |
| Model | trägt die Daten vom Controller zur View, als Paare aus Name und Wert | standort = „Foyer Gebäude A“, anzahlOffen = Zahl der offenen Fahrten |
| View | stellt die Daten dar, hier als HTML | Template flurmonitor.html, gerendert von Thymeleaf |
Das Modul Spring Web MVC steckt schon im Backend, denn auch die REST-Controller laufen darüber. Im Mittelpunkt steht das DispatcherServlet, ein sogenannter Front Controller: Jede HTTP-Anfrage an die Anwendung landet zuerst bei ihm. Es sucht die passende Handler-Methode (über die Annotationen wie @GetMapping), ruft sie auf und kümmert sich danach um die Antwort. Gibt die Methode Daten zurück, wandelt ein Message Converter sie in JSON um. Gibt sie einen View-Namen zurück, fragt das DispatcherServlet einen ViewResolver, welche View dazu gehört, und lässt diese mit den Daten aus dem Model rendern. Vor all dem steht Spring Security als Filter und entscheidet, ob die Anfrage überhaupt durchkommt. Im Sequenzdiagramm oben ist dieser Weg vereinfacht: Eigentlich ruft das DispatcherServlet die gefundene View auf, die wiederum die Template-Engine nutzt.
Die Fachlogik bleibt, wo sie ist: in der Service-Schicht. Der neue Controller nutzt den vorhandenen MonitorService, denselben wie der REST-Controller für /v1/monitor/eintraege. So gilt für beide Wege dieselbe Regel, welche Angebote als offen zählen.
Thymeleaf als Abhängigkeit einbinden
Für den View-Teil braucht Spring eine Template-Engine: ein Programm, das eine HTML-Vorlage mit Platzhaltern und die Daten aus dem Model zu fertigem HTML zusammensetzt. Spring Boot unterstützt mehrere davon, etwa Thymeleaf, FreeMarker, Mustache und Groovy Templates. JSP, die Technik älterer Java-Webanwendungen, funktioniert mit dem eingebetteten Server eines ausführbaren JAR nur eingeschränkt und wird für neue Projekte nicht empfohlen. Das Team wählt Thymeleaf: Seine Templates sind HTML-Dateien mit zusätzlichen Attributen und lassen sich auch ohne Server im Browser öffnen. Das passt, weil die statische Abfahrtsanzeige aus den Grundlagenartikeln schon fertig ist.
Eingebunden wird Thymeleaf über einen Starter, die Version legt Spring Boot fest:
In build.gradle.kts im Block dependencies:
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")Der Starter bringt Thymeleaf selbst und die Verbindung zu Spring MVC mit. Beim Start richtet die Autokonfiguration eine SpringTemplateEngine und einen ThymeleafViewResolver ein. Ohne eine Zeile Konfiguration gelten Standardwerte, die Du in application.properties ändern könntest:
| Eigenschaft | Standardwert | Bedeutung |
|---|---|---|
spring.thymeleaf.prefix |
classpath:/templates/ |
Ordner, in dem die Templates liegen |
spring.thymeleaf.suffix |
.html |
Endung, die an den View-Namen angehängt wird |
spring.thymeleaf.mode |
HTML |
Art der Templates |
spring.thymeleaf.cache |
true |
einmal gelesene Templates werden zwischengespeichert |
spring.thymeleaf.check-template-location |
true |
warnt beim Start, wenn der Template-Ordner fehlt |
Wegen des Caches siehst Du Änderungen an einem Template erst nach einem Neustart der Anwendung. Für die Entwicklung lässt sich der Cache mit spring.thymeleaf.cache=false abschalten; im Betrieb bleibt er an, weil er Zeit spart.
@Controller statt @RestController
Der neue Controller kommt in das Paket web, neben die REST-Controller. Er unterscheidet sich von ihnen in der Annotation und im Rückgabewert:
package de.dbh.mitfahrlabor.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import de.dbh.mitfahrlabor.service.MonitorService;
/** Liefert die Seite für die Flurmonitore als HTML: Daten ins Model, Darstellung im Template flurmonitor.html. */
@Controller
public class FlurmonitorController {
/** Standort des Pilot-Monitors aus Sprint 1; ab MON-05 ist er je Monitor einstellbar. */
static final String STANDORT = "Foyer Gebäude A";
/** Obergrenze wie im API-Vertrag (max höchstens 50). */
private static final int HOECHSTENS = 50;
private final MonitorService monitorService;
public FlurmonitorController(MonitorService monitorService) {
this.monitorService = monitorService;
}
@GetMapping("/flurmonitor")
public String anzeigen(Model model) {
int anzahlOffen = monitorService.naechsteAngebote(null, null, null, HOECHSTENS).size();
model.addAttribute("standort", STANDORT);
model.addAttribute("anzahlOffen", anzahlOffen);
return "flurmonitor"; // View-Name: templates/flurmonitor.html
}
}@Controller markiert die Klasse als Web-Controller und macht sie zur Spring-Bean, genau wie bei den REST-Controllern. Den MonitorService bekommt sie per Konstruktorinjektion. Der Unterschied steckt im Rückgabewert der Methode: Der String "flurmonitor" ist kein Inhalt der Antwort, sondern der Name der View, die Spring rendern soll.
@RestController ist nichts anderes als @Controller plus @ResponseBody. @ResponseBody sagt Spring, dass der Rückgabewert selbst in den Rumpf der Antwort geschrieben wird. Bei einem Objekt oder einer Liste geschieht das als JSON, bei einem String als einfacher Text. Würdest Du den Flurmonitor-Controller versehentlich mit @RestController annotieren, zeigte der Browser deshalb nur das Wort flurmonitor.
@RestController |
@Controller |
|
|---|---|---|
| Rückgabewert der Methode | Daten (Objekt, Liste, ResponseEntity) |
View-Name als String |
| was daraus wird | Message Converter schreibt JSON in die Antwort | ViewResolver sucht das Template, Thymeleaf rendert HTML |
| Content-Type der Antwort | application/json |
text/html |
| typischer Client | Web-App, JavaFX-App, andere Dienste | Browser, der fertige Seiten anzeigt |
| im Backend | MonitorController für /v1/monitor/eintraege |
FlurmonitorController für /flurmonitor |
Beide Controller-Arten dürfen im selben Projekt nebeneinander stehen. Wichtig ist nur, dass sich ihre Pfade nicht überschneiden; die REST-Schnittstelle liegt deshalb unter /v1/…, die Seiten für Menschen ohne Versionsnummer darunter.
Daten ins Model legen
Der Parameter Model model ist ein Beispiel dafür, wie Spring MVC Handler-Methoden versorgt: Steht ein Parameter dieses Typs in der Signatur, legt das DispatcherServlet vor dem Aufruf ein leeres Model an und gibt es hinein. Mit model.addAttribute(name, wert) legt der Controller Werte unter einem Namen ab. Nach dem Aufruf reicht Spring alle Attribute an die View weiter, die sie über diesen Namen anspricht.
Im ersten Schritt legt der Controller zwei Werte ab: den Standort des Monitors und die Zahl der offenen Fahrten, die der MonitorService liefert. Mehr braucht es noch nicht; die Liste der Fahrten selbst kommt im nächsten Artikel ins Model. Zwei Regeln helfen schon jetzt:
Die Attributnamen sind die Schnittstelle zwischen Controller und Template. Ein Tippfehler auf einer Seite fällt nicht beim Kompilieren auf, sondern erst, wenn die Seite leer bleibt. Deshalb gehören sie in einen Test.
Ins Model kommen nur Daten, die die Seite anzeigen darf. Der Flurmonitor zeigt keine Namen (AK-MON-01.3). Was gar nicht erst im Model steht, kann auch kein Template versehentlich ausgeben.
Statt Model erlaubt Spring auch ModelMap, eine einfache Map<String, Object> oder den Rückgabetyp ModelAndView, der View-Namen und Daten in einem Objekt bündelt. In neuem Code ist Model mit einem String als Rückgabewert die übliche Form.
Wie Spring das Template findet
Nach dem Aufruf hat das DispatcherServlet den View-Namen flurmonitor in der Hand. Der ThymeleafViewResolver setzt daraus mit Präfix und Endung den Pfad zusammen:
classpath:/templates/ + flurmonitor + .html = classpath:/templates/flurmonitor.html
classpath: bedeutet: Die Datei wird im Klassenpfad der Anwendung gesucht, also unter src/main/resources/ im Quellcode und im fertigen JAR unter BOOT-INF/classes/. Enthält der View-Name einen Schrägstrich, etwa verwaltung/hinweise, sucht Spring in einem Unterordner: templates/verwaltung/hinweise.html. Die Endung gibst Du im View-Namen nie an, sonst sucht Spring nach flurmonitor.html.html.
Findet Thymeleaf keine Datei, bricht das Rendern mit einer TemplateInputException ab. Im Log steht dann Error resolving template [flurmonitor], template might not exist or might not be accessible by any of the configured Template Resolvers, und der Browser bekommt den Status 500; im Backend kommt er wie alle Fehler als Problem Details im JSON-Format an. Das ist kein 404: Die Anfrage hat einen Controller gefunden, nur seine View fehlt.
Das erste Template aus der statischen Abfahrtsanzeige
Für das Template muss niemand von vorn anfangen. Die Abfahrtsanzeige aus den Bootstrap-Grundlagen ist gültiges HTML, und ein Thymeleaf-Template ist HTML mit zusätzlichen Attributen. Das Team kopiert abfahrtsanzeige.html nach src/main/resources/templates/flurmonitor.html und ändert vier Stellen. Der Ausschnitt zeigt Kopf und Überschrift:
<!DOCTYPE html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mitfahrlabor – Abfahrten am Flurmonitor</title>
<!-- Bootstrap vom CDN wie in der statischen Seite (gekürzt) -->
<link rel="stylesheet" href="../static/css/flurmonitor-bootstrap.css" th:href="@{/css/flurmonitor-bootstrap.css}">
</head>
<body>
<header>
<nav class="navbar navbar-expand-md bg-primary" data-bs-theme="dark" aria-label="Hauptnavigation">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center gap-3" href="flurmonitor.html" th:href="@{/flurmonitor}">
<img src="../static/bilder/mitfahrlabor-logo.svg" th:src="@{/bilder/mitfahrlabor-logo.svg}" alt="" width="64" height="64">
<span class="fs-3 fw-bold">Mitfahrlabor</span>
</a>
<!-- Navigation wie in der statischen Seite, ohne den Link „Hinweis anlegen“ -->
</div>
</nav>
<div class="container-fluid px-4 pt-4">
<h1 class="display-5 fw-bold text-primary mb-1">Abfahrten</h1>
<p class="lead mb-0">Flurmonitor im <span th:text="${standort}">Eingang A</span> des DBH-Berufskollegs</p>
<p class="mb-0">Offene Fahrten insgesamt: <strong th:text="${anzahlOffen}">5</strong></p>
</div>
</header>Die vier Änderungen:
Namensraum:
xmlns:th="http://www.thymeleaf.org"imhtml-Element. Thymeleaf braucht ihn nicht, aber Entwicklungsumgebungen erkennen daran dieth:-Attribute und schlagen sie vor.Werte aus dem Model:
th:text="${standort}"ersetzt beim Rendern den Inhalt des Elements durch den Wert des Model-Attributsstandort. Der Text, der im Template steht („Eingang A“), ist nur ein Beispielwert für die Ansicht ohne Server. Thymeleaf nennt das Natural Templating: Dieselbe Datei ist Entwurf und Vorlage. Wieth:text,th:eachund die übrigen Ausdrücke genau arbeiten, zeigt der nächste Artikel.Links und Ressourcen:
th:href="@{/css/flurmonitor-bootstrap.css}"undth:src="@{…}"ersetzen beim Rendern das normalehrefbzw.src. Ein Ausdruck in@{…}ist ein Link-Ausdruck: Thymeleaf setzt vor einen Pfad mit/den Kontextpfad der Anwendung, falls sie nicht direkt unter/läuft. Das normalehrefbleibt für die Ansicht ohne Server stehen und zeigt auf die Dateien im Quellordner.Was es noch nicht gibt, fliegt raus: Der Link „Hinweis anlegen“ führte zu einer Seite, die erst mit MON-07 entsteht. Ein Link ins Leere wäre auf dem Monitor ein Fehler.
Ein Punkt der Definition of Done ist ein fehlerfreier Durchlauf durch den Nu Html Checker. Geprüft wird dabei das gerenderte HTML, das der Browser bekommt: Es enthält keine th:-Attribute mehr und ist fehlerfrei. Das Template selbst meldet der Checker dagegen mit Fehlern wie „Attribute th:text not allowed“, denn th: ist kein HTML-Attribut. Wer auch das Template streng gültig halten möchte, kann die Schreibweise data-th-text statt th:text verwenden, die Thymeleaf ebenso versteht.
Die Tabellenzeilen mit den Fahrten bleiben vorerst die Beispieldaten der statischen Seite; ein HTML-Kommentar im Template weist darauf hin. Der nächste Artikel ersetzt sie durch th:each über die Liste aus dem Model. Bootstrap selbst kommt in diesem Schritt noch vom CDN wie in der statischen Seite.
Statische Ressourcen ablegen
Stylesheet und Logo ändern sich nicht von Anfrage zu Anfrage. Sie müssen nicht gerendert werden, sondern werden so ausgeliefert, wie sie sind. Spring Boot liefert solche statischen Ressourcen ohne weiteren Code aus, wenn sie in einem dieser Ordner im Klassenpfad liegen: /static, /public, /resources oder /META-INF/resources. Üblich ist static. Der Pfad unterhalb dieses Ordners wird zur URL:
src/main/resources/
├── templates/
│ └── flurmonitor.html → wird gerendert, keine eigene URL
├── static/
│ ├── css/
│ │ └── flurmonitor-bootstrap.css → http://localhost:8080/css/flurmonitor-bootstrap.css
│ └── bilder/
│ └── mitfahrlabor-logo.svg → http://localhost:8080/bilder/mitfahrlabor-logo.svg
├── application.properties
├── data.sql
└── openapi.yaml
Wichtig ist der Unterschied der beiden Ordner. Was unter static/ liegt, schickt Spring Boot unverändert an jeden, der die URL kennt; eine Datei mit th:-Attributen käme dort ungerendert an. Was unter templates/ liegt, hat dagegen keine eigene URL; es wird nur über einen Controller und seinen View-Namen erreicht. Im Template verweisen die Pfade deshalb auf /css/… und /bilder/…, also ohne static.
Das Stylesheet und das Logo kommen unverändert aus dem Ordner src/bootstrap-grundlagen der Grundlagenartikel. Sie enthalten die Farben mit ausreichendem Kontrast und die Grundschrift von 20 px, die MON-02 („aus der Ferne lesbar“) später braucht.
Die Seite ohne Anmeldung freigeben
Startest Du die Anwendung jetzt und rufst http://localhost:8080/flurmonitor auf, erscheint statt der Seite ein Anmeldedialog des Browsers. Grund ist die Sicherheitskonfiguration aus dem Change Request zur Authentifizierung: Jede Anfrage, für die keine eigene Regel gilt, verlangt eine Anmeldung (anyRequest().authenticated()). Ohne Zugangsdaten antwortet Spring Security mit 401 Unauthorized und dem Header WWW-Authenticate: Basic, und auf diesen Header reagiert der Browser mit dem Dialog. Ein Monitor im Foyer kann sich aber nicht anmelden.
Die Lösung ist eine zusätzliche Regel in der SecurityFilterChain. Sie erlaubt GET-Anfragen auf die Seite und die statischen Ressourcen ohne Anmeldung:
http.authorizeHttpRequests(regeln -> regeln
.requestMatchers(HttpMethod.GET, "/v1/haltepunkte/**", "/v1/monitor/**").permitAll()
.requestMatchers(HttpMethod.GET, "/flurmonitor/**", "/css/**", "/bilder/**").permitAll()
.requestMatchers(HttpMethod.POST, "/v1/angebote", "/v1/angebote/*/buchungen").hasRole("NUTZER")
.requestMatchers("/error", "/h2-console/**").permitAll()
.anyRequest().authenticated())Drei Punkte sind dabei wichtig:
Reihenfolge: Spring Security prüft die Regeln von oben nach unten, die erste passende entscheidet. Die neue Regel muss vor
anyRequest()stehen.Muster:
/flurmonitor/**passt auf/flurmonitorselbst und auf alles darunter, etwa die Monitore je Haltepunkt, die mit MON-05 dazukommen./css/**und/bilder/**geben die statischen Dateien frei. Fehlt diese Freigabe, kommt zwar das HTML an, Stylesheet und Logo aber nicht: Die Seite erscheint ungestaltet, und in den Entwicklerwerkzeugen des Browsers stehen die Anfragen mit 401.So eng wie möglich: Freigegeben wird nur
GET, und nur für diese Pfade. Die REST-Schnittstelle zum Anlegen und Buchen und die künftige Verwaltungsseite für Hinweise bleiben geschützt.
Spring Boot bietet für die üblichen Ordner auch PathRequest.toStaticResources().atCommonLocations() an. Es deckt unter anderem /css/**, /js/**, /images/** und /webjars/** ab, aber nicht den Ordner /bilder/**. Die ausdrückliche Liste ist hier deshalb klarer.
Ein Smoke-Test für die Seite
Ein Smoke-Test ist ein grober, schneller Test, der nur prüft, ob das System überhaupt läuft. Der Begriff stammt aus der Prüfung von Hardware: Ein neues Gerät wird eingeschaltet, und man schaut, ob es raucht. Er ersetzt keine gründlichen Tests, aber er schlägt sofort an, wenn das Zusammenspiel grundsätzlich kaputt ist, etwa weil das Template fehlt, der Controller nicht gefunden wird oder die Sicherheitskonfiguration die Seite sperrt. Genau diese drei Fehler entstehen in diesem Schritt leicht.
Der Test startet deshalb den ganzen Anwendungskontext mit @SpringBootTest, aber ohne echten Server. @AutoConfigureMockMvc stellt einen MockMvcTester bereit, der Anfragen direkt an das DispatcherServlet schickt, einschließlich der Filter von Spring Security. Die Uhr steht wie in den System- und Abnahmetests des Backends fest auf Dienstag, 20.10.2026, 06:00 Uhr, damit die Startdaten aus data.sql als offene Fahrten zählen:
package de.dbh.mitfahrlabor.web;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Clock;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.convention.TestBean;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import de.dbh.mitfahrlabor.UhrKonfiguration;
@SpringBootTest
@AutoConfigureMockMvc
class FlurmonitorSmokeTest {
@TestBean
private Clock uhr;
@Autowired
private MockMvcTester mvc;
static Clock uhr() {
return Clock.fixed(Instant.parse("2026-10-20T04:00:00Z"), UhrKonfiguration.ZEITZONE);
}
@Test
void seiteIstOhneAnmeldungErreichbar() {
assertThat(mvc.get().uri("/flurmonitor"))
.hasStatusOk()
.hasContentTypeCompatibleWith(MediaType.TEXT_HTML)
.hasViewName("flurmonitor")
.bodyText()
.contains("<h1", "Abfahrten", "Foyer Gebäude A", "<strong>2</strong>")
.doesNotContain("th:text");
}
@Test
void stylesheetUndLogoSindOhneAnmeldungErreichbar() {
assertThat(mvc.get().uri("/css/flurmonitor-bootstrap.css"))
.hasStatusOk()
.hasContentTypeCompatibleWith("text/css");
assertThat(mvc.get().uri("/bilder/mitfahrlabor-logo.svg")).hasStatusOk();
}
@Test
void andereSeitenVerlangenWeiterhinEineAnmeldung() {
assertThat(mvc.get().uri("/verwaltung/hinweise")).hasStatus(HttpStatus.UNAUTHORIZED);
}
}Der erste Test prüft, was die Seite ausmacht: Status 200, HTML als Inhaltstyp, der richtige View-Name und der Wert aus dem Model. Am 20.10.2026 sind laut data.sql zwei Angebote offen, deshalb steht <strong>2</strong> im HTML. doesNotContain("th:text") stellt sicher, dass Thymeleaf das Template wirklich gerendert hat; Thymeleaf entfernt seine Attribute aus der Ausgabe. Der dritte Test sichert die Gegenrichtung ab: Die neue Freigabe darf nicht mehr öffnen als gewollt. /verwaltung/hinweise gibt es noch nicht, trotzdem antwortet die Anwendung mit 401 und nicht mit 404, denn Spring Security prüft die Anmeldung, bevor das DispatcherServlet nach einem Controller sucht.
Weil der Test keinen @Tag("system") trägt, läuft er in der normalen Testaufgabe mit und zählt für das Abdeckungs-Gate. Er braucht wenige Sekunden länger als ein Komponententest, denn er startet den ganzen Kontext mit Datenbank. Gründliche Tests der Web-Schicht mit @WebMvcTest und gemocktem Service folgen im Artikel zum Integrationstest der Web-Schicht.
Laufen lassen kannst Du den Smoke-Test allein oder mit allen anderen Tests:
.\gradlew.bat test --tests "*FlurmonitorSmokeTest"
.\gradlew.bat checkStarten und ansehen
Gestartet wird das Backend wie bisher:
.\gradlew.bat bootRunUnter http://localhost:8080/flurmonitor erscheint die Seite ohne Anmeldung. Weil die Startdaten im Oktober 2026 liegen, zeigt die Zeile „Offene Fahrten insgesamt“ bei einem Start an einem späteren Tag 0; die Tabelle darunter zeigt noch die Beispieldaten.
Öffnest Du den Quelltext der Seite im Browser, findest Du kein einziges th:-Attribut mehr. Statt href="../static/css/…" steht dort href="/css/flurmonitor-bootstrap.css", statt „Eingang A“ der Standort aus dem Model. Öffnest Du dagegen die Datei flurmonitor.html direkt aus dem Ordner templates, zeigt der Browser die Beispielwerte: Das ist der Entwurf, an dem die Gestaltung weiterarbeiten kann.
Code-Stand
Den vollständigen Stand dieses Artikels findest Du im Ordner src/a02-thymeleaf-einbinden des Backend-Repositorys. Gegenüber dem letzten Stand der Backend-Reihe sind neu bzw. geändert:
| Datei | Änderung |
|---|---|
build.gradle.kts, pom.xml |
Abhängigkeit spring-boot-starter-thymeleaf |
web/FlurmonitorController.java |
neuer @Controller mit GET /flurmonitor |
web/SicherheitsKonfiguration.java |
Regel für /flurmonitor/**, /css/**, /bilder/** |
resources/templates/flurmonitor.html |
erstes Template aus der statischen Abfahrtsanzeige |
resources/static/css/flurmonitor-bootstrap.css, resources/static/bilder/mitfahrlabor-logo.svg |
statische Ressourcen aus den Grundlagenartikeln |
test/…/web/FlurmonitorSmokeTest.java |
Smoke-Test für Seite, Ressourcen und Schutz |
Nächster Schritt im Sprint
Die erste Aufgabe von MON-01 ist erledigt: Die Seite steht, der Smoke-Test ist grün, die Pipeline auch. Im Daily Scrum meldet das Team, dass T1 bis T6 fertig sind und als Merge Request im Review liegen. Als nächste Aufgabe im Sprint Backlog wartet der Umbau der Tabellen: Im Artikel Templates ersetzt th:each die Beispielzeilen durch die offenen Fahrten aus dem Model, th:if blendet den Text für eine leere Liste ein, Fragmente teilen Kopf und Fuß, und Bootstrap kommt als WebJar aus dem eigenen Server statt vom CDN.
In den Übungsaufgaben zu diesem Artikel bringst Du den Weg einer Anfrage in die richtige Reihenfolge, legst eine eigene Seite für die Mitfahrgesuche an und suchst Fehler anhand ihrer Symptome.
Leitfragen
- Welche Aufgaben übernehmen Model, View und Controller in einer Spring-Web-Anwendung?
Der Controller nimmt die Anfrage an, ruft die Fachlogik (Service) auf und wählt die View aus. Das Model trägt die Daten als Paare aus Name und Wert zur View. Die View, hier ein Thymeleaf-Template, stellt die Daten als HTML dar. siehe Abschnitt „Model, View und Controller in Spring MVC“
- Was unterscheidet einen
@Controllervon einem@RestController?
@RestController ist @Controller plus @ResponseBody: Der Rückgabewert wird als Inhalt der Antwort geschrieben, bei Objekten als JSON. Beim @Controller ist ein zurückgegebener String der Name einer View, die ein ViewResolver sucht und die Template-Engine zu HTML rendert. siehe Abschnitt „@Controller statt @RestController“
- Wie kommen Daten aus dem Controller ins Template?
Die Handler-Methode bekommt einen Parameter vom Typ Model und legt mit model.addAttribute("name", wert) Werte ab. Im Template spricht ein Ausdruck wie th:text="${name}" den Wert über denselben Namen an. siehe Abschnitt „Daten ins Model legen“
- Wo sucht Spring Boot das Template zum View-Namen
flurmonitor, und was passiert, wenn es fehlt?
Präfix classpath:/templates/ plus View-Name plus Endung .html, also src/main/resources/templates/flurmonitor.html. Fehlt die Datei, wirft Thymeleaf eine TemplateInputException („Error resolving template“), die Antwort hat den Status 500. siehe Abschnitt „Wie Spring das Template findet“
- Was unterscheidet die Ordner
staticundtemplatesuntersrc/main/resources?
Dateien unter static liefert Spring Boot unverändert unter ihrem Pfad als URL aus (static/css/a.css → /css/a.css). Dateien unter templates haben keine eigene URL; sie werden nur über einen Controller und seinen View-Namen erreicht und vorher gerendert. siehe Abschnitt „Statische Ressourcen ablegen“
- Warum zeigt der Browser beim ersten Aufruf von
/flurmonitoreinen Anmeldedialog, und wie wird die Seite freigegeben?
Die bestehende Regel anyRequest().authenticated() verlangt für alle übrigen Pfade eine Anmeldung; Spring Security antwortet mit 401 und WWW-Authenticate: Basic, worauf der Browser den Dialog zeigt. Eine Regel requestMatchers(HttpMethod.GET, "/flurmonitor/**", "/css/**", "/bilder/**").permitAll() vor anyRequest() gibt Seite und Ressourcen frei. siehe Abschnitt „Die Seite ohne Anmeldung freigeben“
- Was prüft ein Smoke-Test, und warum startet er hier den ganzen Anwendungskontext?
Er prüft grob, ob das System überhaupt funktioniert, hier: Die Seite kommt mit 200 und HTML, Ressourcen sind erreichbar, anderes bleibt geschützt. Nur mit dem ganzen Kontext wirken Controller, Thymeleaf, statische Ressourcen und Spring Security zusammen, deren Zusammenspiel er absichern soll. siehe Abschnitt „Ein Smoke-Test für die Seite“
- Warum zerlegen die Developers MON-01 im Sprint Planning in Aufgaben, und wann ist MON-01 fertig?
Im dritten Teil des Sprint Planning planen die Developers, wie sie die ausgewählten Einträge umsetzen; Aufgaben von höchstens einem Tag machen den Fortschritt im Daily Scrum sichtbar. MON-01 ist erst fertig, wenn alle Abnahmekriterien erfüllt sind und die Definition of Done gilt, also nicht schon nach der ersten Aufgabe. siehe Abschnitt „Stand im Projekt“
Links und weitere Informationen
für den Einstieg: Ein Flurmonitor mit Thymeleaf: das Projekt im Überblick
als Grundlage: HTML-Grundlagen, CSS-Grundlagen und Bootstrap-Grundlagen
zum Szenario: Szenario Mitfahrlabor mit Personas und Flurmonitoren
als Nachschlagewerk: Spring Framework Reference: Spring Web MVC
als Nachschlagewerk: Spring Boot Reference: Servlet Web Applications, Template Engines und statische Inhalte
als Nachschlagewerk: Tutorial: Thymeleaf + Spring
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Thymeleaf ins Backend einbinden: Controller, Model und das erste Template für den Flurmonitor” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/flurmonitor-thymeleaf-einbinden veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-springboot/Backend. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]
