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


Thymeleaf

📍 Deine Position im Kurs

TagThemaStatus
✅ 1Erste REST APIAbgeschlossen
✅ 2Spring Container & DI + CRUDAbgeschlossen
→ 3@Controller & Thymeleaf Basics👉 DU BIST HIER!
4Thymeleaf Forms & MVC-PatternNoch nicht freigeschaltet
5Konfiguration & LoggingNoch nicht freigeschaltet
6DI & AOP im DetailNoch nicht freigeschaltet
7Scopes in SpringNoch nicht freigeschaltet
8WebSocketsNoch nicht freigeschaltet
9JAX-RS in Spring BootNoch nicht freigeschaltet
10Integration & AbschlussNoch 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/persons auf 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:

  • @RestController gibt 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 = JSONResponse = HTML
Für APIsFür Webseiten
Kein Template nötigBraucht 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 → JSON
  • PersonViewController (@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 Liste
  • current → Aktuelles Objekt
  • even / odd → Gerade/Ungerade Zeile
  • first / 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/42id = 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.cssstatic/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:

  • @Controller statt @RestController!
  • Model als Parameter
  • return "view-name" statt Objekt zurückgeben
  • Lombok @RequiredArgsConstructor für DI

Jetzt hast du DREI Controller im Projekt:

  1. HelloController/hello (einfacher String)
  2. PersonController/api/persons (JSON API)
  3. 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:

ProjekttypBest ChoiceWarum?
Admin-ToolsThymeleafSchnell, einfach, keine JS-Komplexität
Interne ToolsThymeleafTeam kann’s warten, kein Build-Step
Public WebsiteThymeleaf + HTMXSEO, Performance, Progressive Enhancement
Complex SPAReact/VueViel Interaktivität, Real-Time
Mobile + WebReact Native + WebCode-Sharing
E-CommerceHybridThymeleaf 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

TagThemaStatus
✅ 1Erste REST APIABGESCHLOSSEN! 🎉
✅ 2Spring Container & DI + CRUDABGESCHLOSSEN! 🎉
✅ 3@Controller & Thymeleaf BasicsABGESCHLOSSEN! 🎉
→ 4Thymeleaf Forms & MVC-PatternAls nächstes
5Konfiguration & LoggingNoch offen
6DI & AOP im DetailNoch offen
7Scopes in SpringNoch offen
8WebSocketsNoch offen
9JAX-RS in Spring BootNoch offen
10Integration & AbschlussNoch 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

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.