Spring Boot Basic – Tag 3 von 10
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting

📍 Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| ✅ 1 | Erste REST API | Abgeschlossen |
| ✅ 2 | Spring Container & DI + CRUD | Abgeschlossen |
| → 3 | @Controller & Thymeleaf Basics | 👉 DU BIST HIER! |
| 4 | Thymeleaf Forms & MVC-Pattern | Noch nicht freigeschaltet |
| 5 | Konfiguration & Logging | Noch nicht freigeschaltet |
| 6 | DI & AOP im Detail | Noch nicht freigeschaltet |
| 7 | Scopes in Spring | Noch nicht freigeschaltet |
| 8 | WebSockets | Noch nicht freigeschaltet |
| 9 | JAX-RS in Spring Boot | Noch nicht freigeschaltet |
| 10 | Integration & Abschluss | Noch nicht freigeschaltet |
Modul: Spring Boot Basic (10 Arbeitstage)
Dein Ziel: Von REST API zu echten Webseiten mit Thymeleaf!
📋 Voraussetzungen für diesen Kurs
Du brauchst:
- ✅ Tag 1 & 2 abgeschlossen
- ✅ Spring Boot läuft auf deinem Rechner
- ✅ PersonService aus Tag 2 mit CRUD-Funktionen (wichtig!)
- ✅ Grundverständnis von HTML
Optional (hilft beim Verständnis):
- Grundkenntnisse in CSS (für das Styling in Schritt 7)
Tag 2 verpasst? Kein Problem! → Hier geht’s zum Blogbeitrag Tag 2
Wichtig: Tag 3 baut direkt auf Tag 2 auf! Der PersonService, den du gestern erstellt hast, wird heute für HTML-Views wiederverwendet!
⚡ Was du heute baust:
Bisher: Deine API gibt JSON zurück – gut für andere Programme, aber kein Mensch kann das direkt im Browser nutzen.
Heute: Du baust dieselbe Person-API, aber mit echten HTML-Seiten! Statt JSON siehst du eine schöne Tabelle im Browser.
Der Wechsel:
Tag 1 & 2: @RestController → JSON → Für APIs Tag 3: @Controller → HTML → Für Menschen!
🎯 Dein Ziel nach 8 Stunden:
- ✅ Du verstehst den Unterschied zwischen @RestController und @Controller
- ✅ Du kannst Thymeleaf Templates erstellen
- ✅ Deine 100 Personen werden in einer schönen HTML-Tabelle angezeigt
- ✅ Du kannst auf Details-Seiten verlinken
- ✅ Du hast eine professionelle Web-Oberfläche mit CSS
Projekt zum Download:
👉 Github Tag3-Spring-Boot-Basic-thymeleaf.zip
👋 Hi, schön dass du wieder dabei bist!
Elyndra hier. Willkommen zurück!
Erinnerst du dich? In Tag 1 und 2 hast du eine REST API gebaut, die JSON zurückgibt. Das ist super für andere Programme – aber wenn DU die Daten im Browser sehen willst, siehst du nur wirres JSON.
Gestern kam Anna aus München zu mir:
„Elyndra, meine API funktioniert perfekt. Aber mein Chef will eine ‚richtige Webseite sehen‘ – nicht nur JSON!“
Kennst du das Problem? Ich wette, DU auch.
Und weißt du was? Genau deshalb lernen wir heute Thymeleaf! Damit du Menschen-freundliche HTML-Seiten bauen kannst.
Lass uns das heute gemeinsam machen! 🔧
😰 Das Problem: Warum du hier bist
Anna aus München schrieb mir:
„Ich rufe
/api/personsauf und bekomme JSON. Aber ich will eine Tabelle sehen, keine Klammern und Kommata!“
Ja, Anna – ich kenne das. Und ich wette, DU auch.
Das ist das Problem:
Dein bisheriger Code (aus Tag 1 & 2):
@RestController
public class PersonController {
@GetMapping("/api/persons")
public List<Person> getAllPersons() {
return persons; // Spring macht daraus JSON
}
}
Browser zeigt:
[{"id":1,"firstname":"Anna","lastname":"Müller",...}]
Nicht schön für Menschen! ❌
Warum das passiert:
@RestControllergibt Daten zurück (JSON/XML)- Spring konvertiert automatisch zu JSON
- Gut für APIs, schlecht für Browser-Nutzer
Und jetzt lösen wir es – zusammen.
🛠️ Lass uns das lösen – Schritt für Schritt
🟢 GRUNDLAGEN (Schritte 1-6)
Was du lernst:
- Unterschied @RestController vs @Controller
- Thymeleaf Setup und Installation
- Model & View Konzept (MVC-Pattern)
- Erstes HTML-Template erstellen
- Thymeleaf Expressions (${…}, @{…})
- Schleifen mit th:each
- Links mit th:href
Ziel: HTML-Tabelle mit allen Personen + Detail-Seiten
Schritt 1: @RestController vs @Controller verstehen
Was du bisher kennst (Tag 1 & 2):
@RestController
public class PersonController {
@GetMapping("/api/persons")
public List<Person> getAllPersons() {
return persons; // Spring macht daraus JSON
}
}
Browser zeigt:
[{"id":1,"firstname":"Anna","lastname":"Müller",...}]
Problem: Nicht schön für Menschen! ❌
Was wir heute bauen:
@Controller // KEIN @RestController!
public class PersonViewController {
@GetMapping("/persons")
public String showPersons(Model model) {
model.addAttribute("persons", persons);
return "persons-list"; // Name des HTML-Templates!
}
}
Browser zeigt: Eine schöne HTML-Tabelle! ✅
Der Unterschied:
| @RestController | @Controller |
|---|---|
| Gibt Daten zurück (JSON) | Gibt View-Namen zurück (String) |
| Response = JSON | Response = HTML |
| Für APIs | Für Webseiten |
| Kein Template nötig | Braucht Template (z.B. Thymeleaf) |
Faustregel:
- API für andere Programme? →
@RestController - Webseite für Menschen? →
@Controller
Wichtig: In unserem Projekt haben wir jetzt BEIDE:
PersonController(@RestController) für/api/persons→ JSONPersonViewController(@Controller) für/persons→ HTML
Das ist Best Practice! Viele Apps haben beides – API UND Weboberfläche.
Schritt 2: Thymeleaf Setup
Was du brauchst:
- Thymeleaf Dependency in pom.xml
- Templates-Ordner
Was du tust:
2.1 Dependency hinzufügen
In pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Was hier passiert:
- Installiert Thymeleaf Template Engine
- Spring Boot konfiguriert automatisch!
- Templates kommen nach
src/main/resources/templates/
2.2 Projekt-Struktur erweitern
src/ ├── main/ │ ├── java/ │ │ └── com/example/helloworldapi/ │ │ ├── controller/ │ │ │ ├── HelloController.java (von Tag 1) │ │ │ ├── PersonController.java (REST - von Tag 1 & 2) │ │ │ └── PersonViewController.java (NEU - HTML Views!) │ │ ├── service/ │ │ │ └── PersonService.java (von Tag 2) │ │ └── model/ │ │ └── Person.java │ └── resources/ │ ├── templates/ (NEU!) │ │ ├── persons-list.html (NEU!) │ │ └── person-details.html (NEU!) │ ├── static/ (NEU!) │ │ └── css/ │ │ └── style.css (NEU!) │ └── application.properties
Siehst du? Wir haben jetzt 3 Controller:
HelloController→/hello(einfacher String)PersonController→/api/persons(JSON für APIs)PersonViewController→/persons(HTML für Menschen)
Schritt 3: Model & View Konzept
Das MVC-Pattern:
Browser-Request
↓
Controller (@Controller)
↓
holt Daten vom Service
↓
packt Daten ins Model
↓
gibt View-Namen zurück
↓
Thymeleaf rendert HTML
↓
Browser bekommt HTML
3.1 Das Model verstehen
@Controller
public class PersonViewController {
private final PersonService personService;
public PersonViewController(PersonService personService) {
this.personService = personService;
}
@GetMapping("/persons")
public String showPersons(Model model) {
// 1. Daten vom Service holen
List<Person> persons = personService.getAllPersons();
// 2. Daten ins Model packen
model.addAttribute("persons", persons);
// ^^^^^^^^ ^^^^^^^
// Name Daten
// 3. View-Namen zurückgeben
return "persons-list";
// ^^^^^^^^^^^^^^
// → src/main/resources/templates/persons-list.html
}
}
Was hier passiert:
Was macht model.addAttribute("persons", persons)?
Das Model ist wie ein Paket, das du ans Template schickst:
Model = {
"persons": [Person1, Person2, Person3, ...]
}
Im Template kannst du dann mit ${persons} darauf zugreifen!
Schritt 4: Erstes Thymeleaf Template
Jetzt fügst du hinzu:
4.1 Einfaches Template erstellen
Erstelle: src/main/resources/templates/persons-list.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Personen-Liste</title>
<link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
<h1>Personen-Liste</h1>
<p>Anzahl Personen: <span th:text="${persons.size()}">0</span></p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Vorname</th>
<th>Nachname</th>
<th>E-Mail</th>
</tr>
</thead>
<tbody>
<!-- Hier kommt die Schleife! -->
</tbody>
</table>
</body>
</html>
Wichtige Thymeleaf-Basics:
xmlns:th="http://www.thymeleaf.org"
- Aktiviert Thymeleaf im Template
- Ermöglicht
th:*Attribute
th:text="${...}"
- Zeigt Werte an
${persons.size()}→ Ruft.size()auf der Liste auf- Ersetzt den Inhalt des Elements
th:href="@{/css/style.css}"
- Erstellt URLs
@{...}ist Thymeleaf URL-Syntax- Spring Boot fügt automatisch Context-Path hinzu
4.2 Thymeleaf Expressions verstehen
Variable Expressions: ${...}
<!-- Einfache Variable -->
<p th:text="${persons.size()}">0</p>
<!-- Objekt-Property -->
<p th:text="${person.firstname}">Max</p>
<!-- Methoden-Aufruf -->
<p th:text="${person.getEmail()}">email@example.com</p>
URL Expressions: @{...}
<!-- Statische Datei -->
<link rel="stylesheet" th:href="@{/css/style.css}">
<!-- Mit Parameter -->
<a th:href="@{/persons/{id}(id=${person.id})}">Details</a>
<!-- Ergebnis: /persons/42 -->
<!-- Mit Query-Parameter -->
<a th:href="@{/persons(page=2)}">Seite 2</a>
<!-- Ergebnis: /persons?page=2 -->
Selection Expressions: *{...}
<!-- Wird später wichtig bei Forms! -->
<div th:object="${person}">
<p th:text="*{firstname}">Vorname</p>
<!-- Entspricht: ${person.firstname} -->
</div>
Schritt 5: Schleifen mit th:each
Jetzt wird’s spannend – wir zeigen alle 100 Personen!
5.1 Einfache Schleife
Update: persons-list.html
<tbody>
<tr th:each="person : ${persons}">
<td th:text="${person.id}">1</td>
<td th:text="${person.firstname}">Max</td>
<td th:text="${person.lastname}">Mustermann</td>
<td th:text="${person.email}">max@example.com</td>
</tr>
</tbody>
Was passiert hier?
th:each="person : ${persons}"
^^^^^^ ^^^^^^^^^^
Variable Liste aus Model
Thymeleaf macht daraus:
<tr>
<td>1</td>
<td>Anna</td>
<td>Müller</td>
<td>anna.mueller@example.com</td>
</tr>
<tr>
<td>2</td>
<td>Max</td>
<td>Schmidt</td>
<td>max.schmidt@example.com</td>
</tr>
<!-- ... 98 weitere Zeilen ... -->
Stefan aus Berlin fragte hier: „Kann ich auch die Zeilennummer anzeigen?“
Gute Frage, Stefan! Ja, mit iterStat:
5.2 Schleifen-Variablen (iterStat)
Thymeleaf bietet zusätzliche Infos zur Schleife:
<tbody>
<tr th:each="person, iterStat : ${persons}">
<td th:text="${iterStat.index}">0</td> <!-- 0, 1, 2, ... -->
<td th:text="${iterStat.count}">1</td> <!-- 1, 2, 3, ... -->
<td th:text="${person.firstname}">Max</td>
<td th:text="${person.lastname}">Mustermann</td>
<td th:text="${person.email}">max@example.com</td>
</tr>
</tbody>
iterStat Properties:
index→ 0-basiert (0, 1, 2, …)count→ 1-basiert (1, 2, 3, …)size→ Größe der Listecurrent→ Aktuelles Objekteven/odd→ Gerade/Ungerade Zeilefirst/last→ Erste/Letzte Zeile
Praktisch für Zebra-Streifen:
<tr th:each="person, iterStat : ${persons}"
th:class="${iterStat.odd} ? 'odd-row' : 'even-row'">
<!-- Zeilen haben abwechselnd 'odd-row' oder 'even-row' Klasse -->
</tr>
Schritt 6: Links mit th:href
Wir fügen Links zu Personen-Details hinzu!
6.1 Details-Link in der Tabelle
<tbody>
<tr th:each="person : ${persons}">
<td th:text="${person.id}">1</td>
<td th:text="${person.firstname}">Max</td>
<td th:text="${person.lastname}">Mustermann</td>
<td th:text="${person.email}">max@example.com</td>
<td>
<a th:href="@{/persons/{id}(id=${person.id})}">Details</a>
</td>
</tr>
</tbody>
URL-Syntax erklärt:
@{/persons/{id}(id=${person.id})}
^^^^^^^^ ^^^^ ^^^ ^^^^^^^^^^^^
Basis Var Wert zuweisen
Ergebnis:
- Person mit ID 1:
/persons/1 - Person mit ID 42:
/persons/42
6.2 Details-Controller erstellen
In PersonViewController.java:
@GetMapping("/persons/{id}")
public String showPersonDetails(@PathVariable Long id, Model model) {
Person person = personService.getPersonById(id);
if (person == null) {
// Person nicht gefunden
return "redirect:/persons";
}
model.addAttribute("person", person);
return "person-details";
}
Neu hier: @PathVariable
@GetMapping("/persons/{id}")
// ^^^^
// Platzhalter
public String showPersonDetails(@PathVariable Long id, ...) {
// ^^^^^^
// Wert aus URL
URL: /persons/42 → id = 42
6.3 Details-Template erstellen
Erstelle: src/main/resources/templates/person-details.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Person Details</title>
<link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
<h1>Person Details</h1>
<div class="person-card">
<p><strong>ID:</strong> <span th:text="${person.id}">1</span></p>
<p><strong>Vorname:</strong> <span th:text="${person.firstname}">Max</span></p>
<p><strong>Nachname:</strong> <span th:text="${person.lastname}">Mustermann</span></p>
<p><strong>E-Mail:</strong> <span th:text="${person.email}">max@example.com</span></p>
</div>
<a th:href="@{/persons}">← Zurück zur Liste</a>
</body>
</html>
Und boom – es funktioniert! ✅
🟡 PROFESSIONAL (Schritte 7-8)
Was du lernst:
- CSS für professionelles Design
- Finaler PersonViewController mit allen Features
- Service-Wiederverwendung (REST + HTML nutzen denselben Service!)
Ziel: Schöne, produktionsreife Web-Oberfläche
Schritt 7: CSS hinzufügen
Lass uns die Seite etwas schöner machen!
Erstelle: src/main/resources/static/css/style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
margin-bottom: 20px;
}
table {
width: 100%;
background-color: white;
border-collapse: collapse;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
thead {
background-color: #4CAF50;
color: white;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
tr:hover {
background-color: #f5f5f5;
}
.odd-row {
background-color: #f9f9f9;
}
.even-row {
background-color: white;
}
a {
color: #4CAF50;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.person-card {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.person-card p {
margin-bottom: 10px;
}
Spring Boot findet das automatisch!
- Alles in
static/ist öffentlich erreichbar /css/style.css→static/css/style.css
Schritt 8: Kompletter PersonViewController
Finaler PersonViewController
Hier ist der finale PersonViewController mit Lombok:
package com.example.helloworldapi.controller;
import com.example.helloworldapi.model.Person;
import com.example.helloworldapi.service.PersonService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
@RequiredArgsConstructor
public class PersonViewController {
// DI mit Lombok - PersonService wird automatisch injiziert
private final PersonService personService;
// Liste aller Personen
@GetMapping("/persons")
public String showPersons(Model model) {
model.addAttribute("persons", personService.getAllPersons());
return "persons-list";
}
// Details einer Person
@GetMapping("/persons/{id}")
public String showPersonDetails(@PathVariable Long id, Model model) {
Person person = personService.getPersonById(id);
if (person == null) {
return "redirect:/persons";
}
model.addAttribute("person", person);
return "person-details";
}
// Startseite (Bonus)
@GetMapping("/")
public String home() {
return "redirect:/persons";
}
}
Wichtig:
@Controllerstatt@RestController!Modelals Parameterreturn "view-name"statt Objekt zurückgeben- Lombok
@RequiredArgsConstructorfür DI
Jetzt hast du DREI Controller im Projekt:
HelloController→/hello(einfacher String)PersonController→/api/persons(JSON API)PersonViewController→/persons(HTML Views)
🌐 Was die Community gefunden hat
Sarah aus Hamburg löste es anders:
„Ich habe Bootstrap für das CSS verwendet – sieht super aus!“
Code Sentinel meinte dazu:
„Sarahs Ansatz ist gut für schnelles Prototyping. Dein Custom CSS gibt dir mehr Kontrolle für Production.“
Was lernst DU daraus?
- Bootstrap: Schneller Start, vorgefertigte Komponenten
- Custom CSS: Mehr Kontrolle, keine Abhängigkeiten
- Mehrere Wege führen zum Ziel!
🔵 BONUS: ERWEITERTE FEATURES
Was du baust:
- Paginierung (10 Personen pro Seite)
- Suchfunktion
- Sortierung nach Spalten
Ziel: Enterprise-Features für echte Anwendungen
Bonus 1: Paginierung hinzufügen
Ziel: Zeige nur 10 Personen pro Seite mit „Weiter/Zurück“ Buttons.
Schritt 1: Controller erweitern
@GetMapping("/persons")
public String showPersons(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Model model
) {
List<Person> allPersons = personService.getAllPersons();
// Berechne Start und End
int start = page * size;
int end = Math.min(start + size, allPersons.size());
// Subliste
List<Person> pagePersons = allPersons.subList(start, end);
model.addAttribute("persons", pagePersons);
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", (int) Math.ceil(allPersons.size() / (double) size));
return "persons-list";
}
Schritt 2: Template erweitern (unter der Tabelle)
<div class="pagination">
<a th:if="${currentPage > 0}"
th:href="@{/persons(page=${currentPage - 1})}">← Zurück</a>
<span>Seite <span th:text="${currentPage + 1}">1</span>
von <span th:text="${totalPages}">10</span></span>
<a th:if="${currentPage + 1 < totalPages}"
th:href="@{/persons(page=${currentPage + 1})}">Weiter →</a>
</div>
Schritt 3: CSS für Pagination
.pagination {
margin-top: 20px;
text-align: center;
}
.pagination a {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border-radius: 4px;
margin: 0 5px;
}
.pagination a:hover {
background-color: #45a049;
}
.pagination span {
padding: 8px 16px;
}
Bonus 2: Suche implementieren
Ziel: Suchfeld für Vor- und Nachnamen.
Schritt 1: Controller anpassen
@GetMapping("/persons")
public String showPersons(
@RequestParam(required = false) String search,
Model model
) {
List<Person> persons;
if (search != null && !search.trim().isEmpty()) {
persons = personService.searchPersons(search);
model.addAttribute("search", search);
} else {
persons = personService.getAllPersons();
}
model.addAttribute("persons", persons);
return "persons-list";
}
Schritt 2: Suchformular im Template (über der Tabelle)
<form method="get" action="/persons" class="search-form">
<input type="text"
name="search"
th:value="${search}"
placeholder="Suche nach Name...">
<button type="submit">Suchen</button>
<a th:href="@{/persons}" th:if="${search}">Zurücksetzen</a>
</form>
Schritt 3: CSS
.search-form {
margin-bottom: 20px;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-form input {
padding: 8px;
width: 300px;
border: 1px solid #ddd;
border-radius: 4px;
}
.search-form button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
Bonus 3: Sortierung nach Spalten
Ziel: Klick auf Spalten-Überschrift sortiert die Tabelle.
Schritt 1: Controller anpassen
@GetMapping("/persons")
public String showPersons(
@RequestParam(defaultValue = "id") String sort,
@RequestParam(defaultValue = "asc") String order,
Model model
) {
List<Person> persons = personService.getSortedPersons(sort, order);
model.addAttribute("persons", persons);
model.addAttribute("currentSort", sort);
model.addAttribute("currentOrder", order);
return "persons-list";
}
Schritt 2: Template Tabellen-Header anpassen
<thead>
<tr>
<th>
<a th:href="@{/persons(sort='id', order=${currentSort == 'id' and currentOrder == 'asc' ? 'desc' : 'asc'})}">
ID
<span th:if="${currentSort == 'id'}"
th:text="${currentOrder == 'asc' ? '↑' : '↓'}"></span>
</a>
</th>
<th>
<a th:href="@{/persons(sort='firstname', order=${currentSort == 'firstname' and currentOrder == 'asc' ? 'desc' : 'asc'})}">
Vorname
<span th:if="${currentSort == 'firstname'}"
th:text="${currentOrder == 'asc' ? '↑' : '↓'}"></span>
</a>
</th>
<th>
<a th:href="@{/persons(sort='lastname', order=${currentSort == 'lastname' and currentOrder == 'asc' ? 'desc' : 'asc'})}">
Nachname
<span th:if="${currentSort == 'lastname'}"
th:text="${currentOrder == 'asc' ? '↑' : '↓'}"></span>
</a>
</th>
<th>
<a th:href="@{/persons(sort='email', order=${currentSort == 'email' and currentOrder == 'asc' ? 'desc' : 'asc'})}">
E-Mail
<span th:if="${currentSort == 'email'}"
th:text="${currentOrder == 'asc' ? '↑' : '↓'}"></span>
</a>
</th>
<th>Aktionen</th>
</tr>
</thead>
Schritt 3: CSS für klickbare Headers
thead th a {
color: white;
text-decoration: none;
display: block;
}
thead th a:hover {
text-decoration: underline;
}
⚠️ Stolpersteine, die du vermeiden solltest
Fehler 1: Template nicht gefunden
Das passiert, wenn du: Template im falschen Ordner ablegst
Symptom:
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [persons-list]
So vermeidest du es:
❌ FALSCH: src/main/resources/static/persons-list.html ✅ RICHTIG: src/main/resources/templates/persons-list.html
Fehler 2: Thymeleaf-Syntax wird als Text angezeigt
Das passiert, wenn du: xmlns:th vergisst
Symptom: Im Browser steht: ${persons.size()} statt 100
So vermeidest du es:
❌ FALSCH: <!DOCTYPE html> <html> ✅ RICHTIG: <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org">
Anna ist darüber gestolpert – aber jetzt weißt DU es besser!
Fehler 3: 404 Error bei /persons
Das passiert, wenn du: @RestController statt @Controller verwendest
So vermeidest du es:
❌ FALSCH:
@RestController // Gibt JSON zurück!
public class PersonViewController {
✅ RICHTIG:
@Controller // Gibt View-Namen zurück!
public class PersonViewController {
Fehler 4: CSS wird nicht geladen
Das passiert, wenn du: CSS im falschen Ordner ablegst
So vermeidest du es:
❌ FALSCH: src/main/resources/templates/css/style.css ✅ RICHTIG: src/main/resources/static/css/style.css
Und im HTML:
❌ FALSCH:
<link rel="stylesheet" href="/static/css/style.css">
✅ RICHTIG:
<link rel="stylesheet" th:href="@{/css/style.css}">
Fehler 5: Liste ist leer (keine Personen angezeigt)
Das passiert, wenn du: Model-Attribut-Name nicht übereinstimmt
So vermeidest du es:
// Controller:
model.addAttribute("persons", personService.getAllPersons());
// ^^^^^^^^
// Template MUSS verwenden:
th:each="person : ${persons}"
// ^^^^^^^^
// Namen müssen EXAKT übereinstimmen!
Fehler 6: Property ‚firstname‘ not found on Person
Das passiert, wenn du: Person-Klasse keine Getter hat
So vermeidest du es:
❌ FALSCH:
public class Person {
private String firstname;
// Kein Getter!
}
✅ RICHTIG (mit Lombok):
@Data
public class Person {
private String firstname;
}
✅ Andere Dokumentationen
- Offizielle Dokumentation des Spring Framework: „The IoC Container” — Einstieg in die Grundlagen von BeanFactory, ApplicationContext, Beans. Home+2Home+2
- Offizielle Dokumentation: „Dependency Injection” — beschreibt die Varianten (Konstruktor-, Setter-Injection) und wie Spring DI technisch umsetzt. Home+1
- Artikel bei Baeldung: „Inversion of Control and Dependency Injection in Spring” — praxisnah mit Erklärungen zu DI-Muster, Annotationen, Komponenten-Scanning. Baeldung on Kotlin
- Tutorial bei Vogella GmbH: „Dependency Injection with the Spring Framework – Tutorial” — Schritt-für-Schritt Beispiel mit DI im Spring Konfigurations- und Annotationskontext. vogella.com
- Tutorial bei Simplilearn: „Spring – IoC Container (With Examples)” — Anschaulich erklärt, wie der Container-Mechanismus funktioniert. Simplilearn.com
✅ Deine Erfolgs-Checkliste
Bevor du weitermachst, check ab:
Grundlagen (🟢):
- [ ] Thymeleaf Dependency ist in pom.xml
- [ ] PersonViewController existiert mit @Controller
- [ ] persons-list.html zeigt Tabelle mit allen Personen
- [ ] person-details.html zeigt Details einer Person
- [ ] Links funktionieren (Klick auf Details → Details-Seite)
- [ ] th:text, th:each, th:href funktionieren
- [ ] Du hast 3 Controller: Hello, PersonREST, PersonView
Professional (🟡):
- [ ] CSS macht die Seite schön
- [ ] Browser zeigt HTML statt JSON
- [ ] Service-Wiederverwendung funktioniert (REST + HTML nutzen denselben Service)
Bonus (🔵):
- [ ] Paginierung funktioniert
- [ ] Suche funktioniert
- [ ] Sortierung funktioniert
Wenn alle Basis-Punkte ✅ sind: Perfekt! Du bist bereit für Tag 4!
Nicht alles funktioniert?
- Schau nochmal in die Troubleshooting-Sektion
- Download das komplette Projekt unten
- Logs in der Console anschauen
❓ Was die Community wissen wollte
Q: Warum xmlns:th="http://www.thymeleaf.org" im HTML?
A: Das aktiviert Thymeleaf. Ohne das funktionieren die th:* Attribute nicht!
Q: Kann ich normales HTML schreiben ohne th:?
A: Ja! Thymeleaf ist „natural templating“ – du kannst die HTML-Datei direkt im Browser öffnen (ohne Server) und siehst die Platzhalter.
Q: Was wenn mein Template nicht gefunden wird?
A: 1) Liegt es in templates/ (NICHT static/)? 2) Heißt die Datei exakt wie der return-Wert + .html? 3) Spring Boot neu starten!
Q: th:text vs th:utext – was ist der Unterschied?
A: th:text escaped HTML (sicher!), th:utext rendert HTML (unsicher wenn User-Input!). Fast immer th:text verwenden!
Q: Kann ich JavaScript in Thymeleaf nutzen?
A: Ja! Normaler <script> Tag funktioniert. Du kannst auch Thymeleaf in JavaScript nutzen: var data = [[${person.firstname}]];
Q: Wie debugge ich Thymeleaf-Fehler?
A: Spring Boot Logs anschauen! Oft steht da „Template persons-list not found“ oder „Property firstname not found on Person“.
Q: Ist Thymeleaf langsamer als React?
A: Initial Render: Nein, oft schneller! Interaktivität: Ja, React ist besser für dynamische UIs. Kommt auf den Use Case an!
Q: Warum haben wir PersonController UND PersonViewController?
A: Best Practice! Manche Clients wollen JSON (API), andere HTML (Browser). So können beide parallel existieren! Das ist wie in echten Firmen.
DU fragst dich vielleicht auch: „Gibt es bei Java Fleet eigentlich auch persönliche Probleme zwischen Projekten?“
Hier die Antwort: Das ist… kompliziert. Manche Geschichten gehören nicht in Tech-Blogs, sondern in private logs. Aber das ist ein anderes Kapitel. 🔒
🔥 Elyndras Real Talk: Die große Thymeleaf vs React Debatte
Die Diskussion in der Kaffeeküche
Marcus kam gestern vorbei, direkt nachdem er von einem Kundentermin zurückkam:
Marcus: „Elyndra, ich hab da eine Frage. Der Kunde heute meinte: ‚Warum baut ihr noch was mit HTML-Templates? Macht man nicht alles mit React und JSON?‘ Was sag ich denen?“
Ich musste lachen. Diese Diskussion hatten wir gefühlt schon hundertmal.
Ich: „Komm, wir holen Code Sentinel dazu. Der hat letzte Woche genau das gleiche Thema bei seinem Fintech-Projekt gehabt.“
Code Sentinel kommt dazu
Code Sentinel (mit seinem Kaffee): „Redet ihr über Server-Side vs Client-Side Rendering? Ich hab gerade drei Stunden damit verbracht, einem Junior zu erklären, warum sein React-Admin-Panel langsamer ist als mein Thymeleaf-Dashboard von 2020.“
Marcus: „Genau das! Der Kunde will ‚modern‘ sein und denkt, Thymeleaf ist Old-School.“
Code Sentinel: „Old-School? Weißt du, was Old-School ist? Performance-Probleme wegen zu viel JavaScript. Warte…“
Er holt sein Laptop und zeigt uns Zahlen:
Admin-Dashboard Projekt (letzten Monat): Thymeleaf-Version: - Initial Load: 180ms - Time to Interactive: 220ms - Bundle Size: 12 KB CSS - JavaScript: 0 KB (außer paar Zeilen für Konfirm-Dialoge) React-Version (vom Junior gebaut): - Initial Load: 1.2s - Time to Interactive: 2.4s - Bundle Size: 340 KB (minified!) - JavaScript: React + React-DOM + Router + State Management + UI Library Beide machen GENAU das Gleiche: CRUD für Admin-Daten.
Marcus: „Krass. Aber React ist doch flexibler?“
Katharina mischt sich ein
Kat (unsere Frontend-Entwicklerin) kommt rein: „Hab ‚React‘ gehört. Verteidigt ihr wieder Thymeleaf?“
Ich: „Nein, wir diskutieren objektiv.“
Kat (grinst): „Klar. Hört zu – ich LIEBE React. Aber wisst ihr, was ich NICHT liebe? React für ein 5-seitiges Admin-Tool zu verwenden. Das ist, als würdest du einen Ferrari kaufen, um damit zum Bäcker zu fahren.“
Code Sentinel: „Exakt! Und dann kommt der Junior und beschwert sich, dass das Deployment kompliziert ist.“
Kat: „Letzte Woche hatte ich so einen Fall. Kunde hat ein internes Tool für Bestellverwaltung. 8 Formulare, 2 Tabellen. Der Praktikant wollte das in Next.js mit TypeScript, Redux Toolkit und styled-components bauen.“
Marcus: „Und?“
Kat: „Ich hab gesagt: ‚Bau’s in Thymeleaf. Du bist in 3 Tagen fertig. Mit React bist du in 2 Wochen fertig und hast danach ein Deployment-Problem.‘ Er hat Thymeleaf genommen. War nach 2 Tagen fertig.“
Die 2025 Realität
Ich: „Okay, lass uns das mal strukturiert aufdröseln. Marcus, zeig das deinem Kunden:“
2025 Architektur-Landscape:
| Projekttyp | Best Choice | Warum? |
|---|---|---|
| Admin-Tools | Thymeleaf | Schnell, einfach, keine JS-Komplexität |
| Interne Tools | Thymeleaf | Team kann’s warten, kein Build-Step |
| Public Website | Thymeleaf + HTMX | SEO, Performance, Progressive Enhancement |
| Complex SPA | React/Vue | Viel Interaktivität, Real-Time |
| Mobile + Web | React Native + Web | Code-Sharing |
| E-Commerce | Hybrid | Thymeleaf für SEO, React für Checkout |
Code Sentinel: „Und dann gibt’s noch den Hidden Cost:“
React-Projekt: - Node.js + npm Setup - Webpack/Vite Config - State Management (Redux/Zustand/...) - Routing Setup - CSS-in-JS oder CSS Modules - Testing Setup (Jest, React Testing Library) - TypeScript Config - ESLint + Prettier - CI/CD für Frontend-Build - Deployment: Separate Static Hosting Thymeleaf-Projekt: - Maven Dependency - Template erstellen - Deployment: Wird mit dem JAR ausgeliefert - FERTIG.
Marcus: „Okay, aber React hat doch die bessere Developer Experience?“
Luca kommt zur Diskussion
Luca (unser DevOps-Engineer): „Hab ‚Developer Experience‘ gehört. Darf ich was sagen?“
Alle: „Klar!“
Luca: „Weißt du, was keine gute Developer Experience ist? Wenn dein CI/CD-Build 8 Minuten für den npm-Build braucht, während der Java-Build in 2 Minuten durch ist. Oder wenn du 3 verschiedene Versionen von Node.js für verschiedene Projekte managen musst.“
Code Sentinel: „Oder wenn das Frontend-Team sich über CORS-Probleme beschwert, weil sie lokal gegen die API entwickeln.“
Kat: „Hey, CORS ist real! Aber… stimmt schon. Bei Thymeleaf läuft alles auf demselben Server. Kein CORS, keine Proxy-Config.“
Die Praxis-Beispiele
Ich: „Marcus, hier sind drei echte Projekte von letztem Monat:“
Projekt 1: Firmen-Intranet (Mittelstand, 200 Mitarbeiter)
Anforderung: - News-Feed - Dokumenten-Verwaltung - Mitarbeiter-Verzeichnis - Urlaubsantrag-System Kunde fragte: "Können wir das in React machen?" Meine Antwort: "Können wir. Aber warum?" Ergebnis: Thymeleaf + HTMX Zeit: 3 Wochen Kosten: 15k€ Wartung: Junior kann's bedienen Wenn wir React genommen hätten: Zeit: 6-8 Wochen Kosten: 30k€ Wartung: Braucht Frontend-Spezialist
Projekt 2: E-Commerce Admin-Panel
Anforderung: - Produktverwaltung - Bestellübersicht - Kunden-Datenbank - Statistiken Architektur: - Public Shop: React (wegen interaktiver Produkt-Filter) - Admin-Panel: Thymeleaf (weil intern, CRUD-heavy) Warum beide? - Shop braucht: SEO, Speed, Interaktivität - Admin braucht: Schnelle Entwicklung, Stabilität Team: Code Sentinel (Backend + Admin), Kat (Shop-Frontend) Beide glücklich. Beide effizient.
Projekt 3: Dashboard für IoT-Daten (Real-Time)
Anforderung: - Live-Graphen - WebSocket-Updates - Karten-Visualisierung - Alarm-System Hier: React war die RICHTIGE Wahl! Warum? - WebSocket-Integration - Real-Time Charts - Komplexe State-Updates - Interaktive Map Thymeleaf hätte hier NICHT funktioniert.
Code Sentinel’s Metrics
Code Sentinel: „Ich hab mal meine Projekte von 2024 analysiert:“
15 Projekte gebaut: 8x Thymeleaf: - Ø Entwicklungszeit: 2.3 Wochen - Ø Bugs nach Launch: 3 - Ø Time to Fix: 1 Stunde - Performance-Score: 95/100 - Wartungsaufwand: Niedrig 5x React: - Ø Entwicklungszeit: 5.1 Wochen - Ø Bugs nach Launch: 12 - Ø Time to Fix: 4 Stunden - Performance-Score: 78/100 - Wartungsaufwand: Mittel 2x Hybrid (Thymeleaf + React Widgets): - Ø Entwicklungszeit: 4.2 Wochen - Ø Bugs nach Launch: 7 - Ø Time to Fix: 2.5 Stunden - Performance-Score: 88/100 - Wartungsaufwand: Mittel Wichtig: Die React-Projekte BRAUCHTEN React! Die Thymeleaf-Projekte hätten mit React LÄNGER gedauert.
Die Entscheidungs-Matrix
Ich: „Marcus, hier ist deine Antwort für den Kunden:“
Nutze Thymeleaf wenn:
- ✅ Hauptsächlich CRUD-Operationen
- ✅ Formulare und Tabellen
- ✅ SEO wichtig ist
- ✅ Server-Side Logik dominiert
- ✅ Team kennt Java, aber kein React
- ✅ Schnelle Entwicklung wichtig
- ✅ Einfaches Deployment gewünscht
- ✅ Interne Tools / Admin-Panels
- ✅ Budget ist begrenzt
- ✅ Wartung durch Backend-Team
Nutze React wenn:
- ✅ Viel Interaktivität (Drag & Drop, etc.)
- ✅ Real-Time Updates (WebSockets)
- ✅ Komplexe UI-State
- ✅ Mobile App geplant (React Native)
- ✅ Team hat Frontend-Skills
- ✅ SPA-Architektur sinnvoll
- ✅ Offline-Funktionalität
- ✅ Progressive Web App
- ✅ Animationen & Transitions wichtig
- ✅ API wird auch von anderen genutzt
Der Kompromiss: Das Beste aus beiden Welten
Ich: „Weißt du, was cool ist? Du musst dich nicht entscheiden!“
Marcus: „Wie meinst du das?“
Ich: „Schau dir unser heutiges Projekt an. Wir haben:“
/api/persons → REST Controller (JSON für externe Apps) /persons → View Controller (HTML für Menschen) Beide nutzen denselben Service! Das heißt: - Externe Systeme können die API nutzen - Interne Mitarbeiter nutzen die Web-UI - Wenn später jemand ein React-Frontend will: API ist da! - Für's Admin-Tool reicht HTML völlig
Code Sentinel: „Genau! Das ist moderne Architektur. Nicht entweder/oder, sondern sowohl/als auch.“
Kat: „Und wenn du später sagst: ‚Dieses eine Feature braucht React‘, dann baust du ein React-Widget ein. Thymeleaf kann React-Components embedden!“
Luca: „Und das Deployment? Ein einziges JAR. Keine zwei getrennten Deployments.“
Marcus‘ Fazit
Marcus: „Okay, ich hab’s verstanden. Ist wie beim Holz – das richtige Werkzeug für den Job!“
Code Sentinel: „Exakt. Du würdest ja auch keinen Vorschlaghammer nehmen, um einen Nagel einzuschlagen.“
Kat: „Obwohl… wenn der Nagel groß genug ist…“ (grinst)
Ich: „Das Wichtigste ist: Beide sind legitim. React ist nicht ‚besser‘ als Thymeleaf. Und Thymeleaf ist nicht ‚besser‘ als React. Sie sind für verschiedene Probleme da.“
Marcus: „Und für Admin-Tools und interne Systeme ist Thymeleaf oft die schnellere, einfachere Lösung.“
Alle: „Genau!“
Die Wahrheit über 2025
Ich: „Weißt du, was die echte 2025-Realität ist, Marcus?“
Marcus: „Sag mir.“
Ich: „Die meisten erfolgreichen Firmen nutzen BEIDES:“
- Google: Server-Side für Search, React für Gmail/Docs
- Amazon: Server-Side für Produktseiten (SEO!), React für Checkout
- Netflix: Server-Side für Landing, React für App
- Spotify: Web Player in React, Admin-Tools in… rate mal… Server-Side Templates!
Code Sentinel: „Niemand ist 100% React. Und niemand sollte 100% Thymeleaf sein. Es geht um die richtige Balance.“
Für DICH bedeutet das
Warum ist Tag 3 wichtig?
Weil DU jetzt beides kannst:
- Tag 1 & 2: REST API mit JSON → Für andere Programme
- Tag 3: HTML-Views mit Thymeleaf → Für Menschen
Das ist moderne Java-Entwicklung! Nicht entweder/oder, sondern sowohl/als auch.
Und das Beste:
Du verstehst jetzt, WANN du WAS einsetzt. Nicht weil’s hip ist. Nicht weil’s im CV gut aussieht. Sondern weil’s das richtige Tool für den Job ist.
Marcus‘ Lektion für heute: „Das richtige Werkzeug für den Job.“
Meine Lektion für dich: „Du hast jetzt das richtige Werkzeug im Werkzeugkasten. Nutze es weise!“
P.S.: Code Sentinel schickte mir später noch diese Nachricht:
„Übrigens, das React-Projekt vom Junior? Der baut jetzt das nächste Admin-Tool in Thymeleaf. Seine Worte: ‚Ich hatte vergessen, wie schnell man sein kann, wenn man nicht 3 Stunden mit npm-Problemen kämpft.'“
Keep coding, keep learning – und wähle deine Tools weise! 💙
📅 Nächster Kurstag: Tag 4
Morgen im Kurs / Nächster Blogbeitrag:
„Thymeleaf Forms & MVC-Pattern komplett“
Was du lernen wirst:
- Formulare mit Thymeleaf erstellen
- th:object, th:field, th:action verstehen
- POST Handling mit @PostMapping (nutzt das CRUD aus Tag 2!)
- Validation Basics (@Valid, @NotBlank)
- Das komplette MVC-Pattern in Aktion
Warum wichtig? Morgen kannst du Personen HINZUFÜGEN über ein Formular. Dann hast du CRUD komplett – API (Tag 2) UND Web-Interface (Tag 4)!
Voraussetzung: Tag 3 abgeschlossen
Bereite dich vor: Stelle sicher, dass deine persons-list.html funktioniert!
👉 Zum Blogbeitrag Tag 4 (erscheint morgen)
📚 Deine Fortschritts-Übersicht
| Tag | Thema | Status |
|---|---|---|
| ✅ 1 | Erste REST API | ABGESCHLOSSEN! 🎉 |
| ✅ 2 | Spring Container & DI + CRUD | ABGESCHLOSSEN! 🎉 |
| ✅ 3 | @Controller & Thymeleaf Basics | ABGESCHLOSSEN! 🎉 |
| → 4 | Thymeleaf Forms & MVC-Pattern | Als nächstes |
| 5 | Konfiguration & Logging | Noch offen |
| 6 | DI & AOP im Detail | Noch offen |
| 7 | Scopes in Spring | Noch offen |
| 8 | WebSockets | Noch offen |
| 9 | JAX-RS in Spring Boot | Noch offen |
| 10 | Integration & Abschluss | Noch offen |
Du hast 30% des Kurses geschafft! 💪
Alle Blogbeiträge dieser Serie:
👉 Spring Boot Basic – Komplette Übersicht
📥 Download & Ressourcen
Projekt zum Download:
👉 Github Tag3-Spring-Boot-Basic-thymeleaf.zip (Stand: 12.11.2025)
Was ist im ZIP enthalten:
- ✅ Komplettes Maven-Projekt
- ✅ PersonViewController implementiert
- ✅ PersonController (REST API) bleibt erhalten
- ✅ HelloController bleibt erhalten
- ✅ Thymeleaf Templates (persons-list.html, person-details.html)
- ✅ CSS für schönes Styling
- ✅ PersonService aus Tag 2
- ✅ README mit Schnellstart
Projekt starten:
# ZIP entpacken # In NetBeans öffnen: File → Open Project # Oder im Terminal: mvn spring-boot:run # Im Browser öffnen: http://localhost:8080/persons # HTML-Ansicht http://localhost:8080/api/persons # JSON API (funktioniert noch!)
Probleme? Issue melden oder schreib mir: elyndra@java-developer.online
💬 Du bist dran!
Hast du es ausprobiert?
Schreib mir in die Comments oder per Mail: elyndra.valen@java-developer.online
Bist du auf Probleme gestoßen?
Erzähl mir davon – vielleicht wird dein Problem der nächste Blogpost!
Hast du eine bessere Lösung gefunden?
Die Community und ich wollen sie hören!
🎉 Das war Tag 3 von Spring Boot Basic!
Du kannst jetzt:
- ✅ @Controller vs @RestController unterscheiden
- ✅ Thymeleaf Templates erstellen
- ✅ Model & View Konzept anwenden
- ✅ HTML-Seiten mit dynamischen Daten (th:text, th:each)
- ✅ Links mit th:href erstellen
- ✅ Schöne Webseiten statt nur JSON!
- ✅ Beide parallel nutzen: REST API UND HTML-Views!
Morgen lernst du Formulare – dann kannst du auch Daten HINZUFÜGEN über die Web-Oberfläche! 🚀
Keep coding, keep learning!
🖖 Live long and prosper
📧 Email: elyndra.valen@java-developer.online
🌐 Website: https://www.java-developer.online
💻 GitHub: https://github.com/ElyndraValen
Tag 4 erscheint morgen. Bis dahin: Happy Coding!
P.S.: Manchmal verstecken sich die interessantesten Geschichten nicht in Code-Repositories, sondern in den… nun ja, private logs. Probier mal die Suche oben auf java-developer.online! 😉
Tags: #SpringBoot #Thymeleaf #MVC #Controller #HTML #Tag3 #JavaFleet

