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


Thymeleaf Forms

📍 Deine Position im Kurs

TagThemaStatus
✅ 1Erste REST APIAbgeschlossen
✅ 2Spring Container & DI + CRUDAbgeschlossen
✅ 3@Controller & Thymeleaf BasicsAbgeschlossen
→ 4Thymeleaf Forms & MVC-Pattern👉 DU BIST HIER!
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: Production-Ready CRUD mit Validation & Error-Handling!


📋 Voraussetzungen

Du brauchst:

  • ✅ Tag 1, 2 & 3 abgeschlossen
  • ✅ PersonService mit CRUD-Methoden (aus Tag 2)
  • ✅ PersonViewController mit GET-Mappings (aus Tag 3)
  • ✅ Grundverständnis von HTML-Formularen

Tag 3 verpasst?Hier geht’s zum Blogbeitrag Tag 3


⚡ Was du heute baust:

Bisher:

  • Tag 2: PersonService mit CRUD (createPerson, updatePerson, deletePerson)
  • Tag 3: PersonViewController zeigt Daten (nur lesend)

Heute: Du baust Formulare für Create, Update, Delete + Production-Ready Features!

Die Sourcen für den heutigen Tag kannst du auf meinem Github Account hier herunterladen.

Die komplette Architektur:

REST API (Tag 2):
  POST   /api/persons        → personService.createPerson()
  PUT    /api/persons/{id}   → personService.updatePerson()
  DELETE /api/persons/{id}   → personService.deletePerson()

HTML-Views (Tag 3+4):
  GET  /persons              → Liste anzeigen
  GET  /persons/{id}         → Details anzeigen
  GET  /persons/new          → Formular anzeigen
  POST /persons/new          → personService.createPerson()
  GET  /persons/{id}/edit    → Formular anzeigen
  POST /persons/{id}/edit    → personService.updatePerson()
  POST /persons/{id}/delete  → personService.deletePerson()

Beide nutzen denselben Service! Das ist Best Practice! 🎯


🎯 Dein Lernpfad heute:

Du arbeitest heute in mehreren aufbauenden Schwierigkeitsstufen. Arbeite in deinem eigenen Tempo durch die Schritte:

🟢 Grundlagen (Schritte 1-4)

Was du lernst:

  • HTML-Formulare mit Thymeleaf erstellen
  • th:object, th:field, th:action beherrschen
  • @PostMapping für Formulare nutzen
  • CRUD komplett (CREATE, UPDATE, DELETE)
  • Das komplette MVC-Pattern verstehen

Ziel: Voll funktionsfähiges CRUD-System mit HTML-Interface

🟡 Professional (Schritte 5-7)

Was du lernst:

  • Production-Ready Validation (Standard + Custom Validators)
  • Validation Groups (Create vs Update)
  • Professional Error-Handling (Custom 404/500 Pages)
  • Whitespace-Trimming (@InitBinder)

Ziel: Code ist bereit für den Produktiv-Einsatz

🔵 Bonus: Eigenes Projekt (Schritt 8)

Was du baust:

  • Komplett eigenständiges Buchbewertungs-System
  • Alle gelernten Techniken anwenden
  • Selbstständig Lösungen entwickeln

Ziel: Sicheres Anwenden aller Tag-4-Konzepte

💡 Tipp: Die Grundlagen (Schritte 1-4) sind essenziell. Die Professional-Features und das Bonus-Projekt kannst du je nach deinem Lernrhythmus angehen!


📦 Benötigte Imports (für später)

Speichere diese – du brauchst sie im Laufe des Tages:

// Validation
import jakarta.validation.constraints.*;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.BindingResult;

// Error Handling
import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;
import org.springframework.boot.web.servlet.error.ErrorController;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;

// Whitespace Trimming
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;

💻 Los geht’s!

🟢 GRUNDLAGEN

Schritt 1: Formular-Grundlagen verstehen

HTML-Formular ohne Thymeleaf:

<form action="/persons/new" method="post">
    <input type="text" name="firstname" />
    <input type="text" name="lastname" />
    <input type="email" name="email" />
    <button type="submit">Speichern</button>
</form>

Problem:

  • Manuelles Mapping von Formular → Java-Objekt
  • Kein Pre-Filling beim Bearbeiten
  • Fehler-Handling kompliziert

Lösung: Thymeleaf Forms!

<form th:action="@{/persons/new}" th:object="${person}" method="post">
    <input type="text" th:field="*{firstname}" />
    <input type="text" th:field="*{lastname}" />
    <input type="email" th:field="*{email}" />
    <button type="submit">Speichern</button>
</form>

Was macht das?

  • th:object="${person}" → Bindet Formular an Person-Objekt
  • th:field="*{firstname}" → Automatisches Mapping (NEU!)
  • th:action="@{/persons/new}" → URL für POST-Request

Schritt 2: Create-Formular (Neue Person)

2.1 Controller-Methoden

Erweitere PersonViewController:

@Controller
@RequiredArgsConstructor  // Lombok Constructor Injection (kennst du aus Tag 2)
public class PersonViewController {
    
    private final PersonService personService;
    
    // ... bisherige Methoden aus Tag 3 ...
    
    // NEU: Formular anzeigen
    @GetMapping("/persons/new")
    public String showCreateForm(Model model) {
        model.addAttribute("person", new Person());
        return "person-form";
    }
    
    // NEU: Formular-Daten verarbeiten
    @PostMapping("/persons/new")
    public String createPerson(@ModelAttribute Person person) {
        personService.createPerson(person);  // Service aus Tag 2!
        return "redirect:/persons";
    }
}

Neu:

  • @GetMapping → zeigt leeres Formular
  • @PostMapping → speichert Daten
  • @ModelAttribute → Spring füllt automatisch Person

2.2 Formular-Template

Erstelle: templates/person-form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Person hinzufügen</title>
    <link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
    <h1>Neue Person hinzufügen</h1>
    
    <form th:action="@{/persons/new}" th:object="${person}" method="post">
        
        <div class="form-group">
            <label for="firstname">Vorname: *</label>
            <input type="text" id="firstname" th:field="*{firstname}" 
                   placeholder="z.B. Max" required />
        </div>
        
        <div class="form-group">
            <label for="lastname">Nachname: *</label>
            <input type="text" id="lastname" th:field="*{lastname}"
                   placeholder="z.B. Mustermann" required />
        </div>
        
        <div class="form-group">
            <label for="email">E-Mail: *</label>
            <input type="email" id="email" th:field="*{email}"
                   placeholder="max@example.com" required />
        </div>
        
        <div class="form-actions">
            <button type="submit" class="btn-primary">Speichern</button>
            <a th:href="@{/persons}" class="btn-secondary">Abbrechen</a>
        </div>
    </form>
</body>
</html>

Wichtig:

  • th:field="*{firstname}" → Automatisches Mapping (name, id, value)
  • Bezieht sich auf th:object="${person}"

2.3 Link zur Liste hinzufügen

Erweitere: persons-list.html (nach <h1>)

<div class="actions">
    <a th:href="@{/persons/new}" class="btn-primary">+ Neue Person</a>
</div>

2.4 CSS für Formulare

Erstelle: static/css/style.css (neue Regeln)

/* Formular-Styling */
.form-group {
    margin-bottom: 20px;
}

.form-group label {
    display: block;
    font-weight: bold;
    margin-bottom: 5px;
    color: #333;
}

.form-group input {
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 16px;
}

.form-group input:focus {
    outline: none;
    border-color: #4CAF50;
}

.form-actions {
    margin-top: 30px;
    display: flex;
    gap: 10px;
}

.btn-primary {
    padding: 10px 20px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
}

.btn-primary:hover {
    background-color: #45a049;
}

.btn-secondary {
    padding: 10px 20px;
    background-color: #ddd;
    color: #333;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
}

.btn-secondary:hover {
    background-color: #ccc;
}

.actions {
    margin-bottom: 20px;
}

Schritt 3: Update-Formular (Person bearbeiten)

3.1 Controller-Methoden

Erweitere PersonViewController:

// Formular zum Bearbeiten anzeigen
@GetMapping("/persons/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model) {
    Person person = personService.getPersonById(id);
    
    if (person == null) {
        return "redirect:/persons";
    }
    
    model.addAttribute("person", person);
    return "person-edit-form";
}

// Bearbeitete Daten speichern
@PostMapping("/persons/{id}/edit")
public String updatePerson(@PathVariable Long id, @ModelAttribute Person person) {
    personService.updatePerson(id, person);
    return "redirect:/persons/" + id;
}

Neu: Pre-Filled Formular mit existierenden Daten!


3.2 Edit-Template

Erstelle: templates/person-edit-form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Person bearbeiten</title>
    <link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
    <h1>Person bearbeiten</h1>
    
    <form th:action="@{/persons/{id}/edit(id=${person.id})}" 
          th:object="${person}" method="post">
        
        <input type="hidden" th:field="*{id}" />
        
        <div class="form-group">
            <label for="firstname">Vorname: *</label>
            <input type="text" id="firstname" th:field="*{firstname}" required />
        </div>
        
        <div class="form-group">
            <label for="lastname">Nachname: *</label>
            <input type="text" id="lastname" th:field="*{lastname}" required />
        </div>
        
        <div class="form-group">
            <label for="email">E-Mail: *</label>
            <input type="email" id="email" th:field="*{email}" required />
        </div>
        
        <div class="form-actions">
            <button type="submit" class="btn-primary">Änderungen speichern</button>
            <a th:href="@{/persons/{id}(id=${person.id})}" class="btn-secondary">Abbrechen</a>
        </div>
    </form>
</body>
</html>

Wichtig: <input type="hidden" th:field="*{id}" /> → ID mitschicken!


3.3 Edit-Link in Details-Seite

Erweitere: person-details.html (nach der person-card)

<div class="actions">
    <a th:href="@{/persons/{id}/edit(id=${person.id})}" class="btn-primary">Bearbeiten</a>
    <a th:href="@{/persons}" class="btn-secondary">Zurück zur Liste</a>
</div>

Schritt 4: Delete-Funktion

4.1 Controller-Methode

Erweitere PersonViewController:

@PostMapping("/persons/{id}/delete")
public String deletePerson(@PathVariable Long id) {
    personService.deletePerson(id);
    return "redirect:/persons";
}

Warum POST statt DELETE? HTML-Formulare unterstützen nur GET und POST!


4.2 Delete-Button

Erweitere: person-details.html (in der actions div)

<div class="actions">
    <a th:href="@{/persons/{id}/edit(id=${person.id})}" class="btn-primary">Bearbeiten</a>
    
    <form th:action="@{/persons/{id}/delete(id=${person.id})}" 
          method="post" 
          style="display: inline;"
          onsubmit="return confirm('Person wirklich löschen?');">
        <button type="submit" class="btn-danger">Löschen</button>
    </form>
    
    <a th:href="@{/persons}" class="btn-secondary">Zurück zur Liste</a>
</div>

4.3 CSS für Delete-Button

Ergänze in style.css:

.btn-danger {
    padding: 10px 20px;
    background-color: #f44336;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.btn-danger:hover {
    background-color: #da190b;
}

🟡 PROFESSIONAL

Schritt 5: Validation komplett – Production-Ready

5.1 Validation Dependency

Prüfe pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Falls nicht vorhanden, hinzufügen!


5.2 Standard-Validation-Annotations (Übersicht)

String-Validierung:

@NotNull        // Nicht null (aber "" ist okay!)
@NotEmpty       // Nicht null ODER leer (aber "  " ist okay!)
@NotBlank       // Nicht null, leer ODER nur Whitespace (BESTE WAHL!)
@Size(min, max) // Länge zwischen min-max
@Email          // Gültige E-Mail
@Pattern(regex) // Regex-Pattern

Zahlen-Validierung:

@Min(1)         // Minimum-Wert
@Max(100)       // Maximum-Wert
@Positive       // > 0
@PositiveOrZero // >= 0

Datum-Validierung:

@Past           // Vergangenheit
@Future         // Zukunft
@PastOrPresent  // Vergangenheit oder heute

5.3 Person-Klasse mit Validierung

Erweitere Person.java:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    
    private Long id;
    
    @NotBlank(message = "Vorname darf nicht leer sein")
    @Size(min = 2, max = 50, message = "Vorname: 2-50 Zeichen")
    @Pattern(regexp = "^[a-zA-ZäöüÄÖÜß\\s-]+$", 
             message = "Vorname darf nur Buchstaben enthalten")
    private String firstname;
    
    @NotBlank(message = "Nachname darf nicht leer sein")
    @Size(min = 2, max = 50, message = "Nachname: 2-50 Zeichen")
    @Pattern(regexp = "^[a-zA-ZäöüÄÖÜß\\s-]+$",
             message = "Nachname darf nur Buchstaben enthalten")
    private String lastname;
    
    @NotBlank(message = "E-Mail darf nicht leer sein")
    @Email(message = "Bitte gültige E-Mail eingeben")
    private String email;
    
    // NEU: Telefon (kommt in 5.4)
    private String phone;
}

Neu: @Pattern für Regex-Validierung!


5.4 Custom Validator erstellen

Schritt 1: Custom Annotation

Erstelle: validation/ValidGermanPhone.java

package com.example.helloworldapi.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = GermanPhoneValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidGermanPhone {
    
    String message() default "Ungültige deutsche Telefonnummer";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
}

Schritt 2: Validator-Klasse

Erstelle: validation/GermanPhoneValidator.java

package com.example.helloworldapi.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class GermanPhoneValidator implements ConstraintValidator<ValidGermanPhone, String> {
    
    private static final String PATTERN = "^(\\+49|0)[1-9]\\d{1,14}$|^(\\+49|0)[1-9][\\d\\s\\-/]{4,20}$";
    
    @Override
    public boolean isValid(String phone, ConstraintValidatorContext context) {
        if (phone == null || phone.trim().isEmpty()) {
            return true;  // Null ist okay (verwende @NotBlank für Pflicht)
        }
        
        String cleaned = phone.replaceAll("[\\s\\-/]", "");
        return cleaned.matches(PATTERN);
    }
}

Schritt 3: In Person nutzen

@ValidGermanPhone
private String phone;

5.5 Controller mit Validation

Erweitere PersonViewController:

// CREATE mit Validation
@PostMapping("/persons/new")
public String createPerson(@Valid @ModelAttribute Person person, 
                          BindingResult result) {
    if (result.hasErrors()) {
        return "person-form";  // Bei Fehlern: Formular erneut
    }
    
    personService.createPerson(person);
    return "redirect:/persons";
}

// UPDATE mit Validation
@PostMapping("/persons/{id}/edit")
public String updatePerson(@PathVariable Long id, 
                          @Valid @ModelAttribute Person person,
                          BindingResult result) {
    if (result.hasErrors()) {
        return "person-edit-form";
    }
    
    personService.updatePerson(id, person);
    return "redirect:/persons/" + id;
}

Neu:

  • @Valid → triggert Validierung
  • BindingResult → enthält Fehler (muss direkt nach @Valid!)

5.6 ControllerAdvice für bessere Error-Messages

Erstelle: controller/GlobalExceptionHandler.java

package com.example.helloworldapi.controller;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.validation.BindException;

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(BindException.class)
    public String handleValidationException(BindException ex, Model model) {
        model.addAttribute("errors", ex.getAllErrors());
        model.addAttribute("person", ex.getTarget());
        return "person-form";
    }
}

Macht: Fängt alle Validation-Fehler global ab!


5.7 Validation Groups

Verschiedene Regeln für Create vs Update!

Schritt 1: Groups definieren

package com.example.helloworldapi.validation;

public interface ValidationGroups {
    interface Create {}
    interface Update {}
}

Schritt 2: In Person nutzen

@NotBlank(message = "E-Mail darf nicht leer sein",
          groups = ValidationGroups.Create.class)  // Nur bei CREATE Pflicht!
@Email(message = "Bitte gültige E-Mail eingeben")
private String email;

Schritt 3: Im Controller nutzen

// CREATE mit Create-Group
@PostMapping("/persons/new")
public String createPerson(@Validated(ValidationGroups.Create.class) @ModelAttribute Person person,
                          BindingResult result) {
    // E-Mail ist Pflicht!
}

// UPDATE mit Update-Group
@PostMapping("/persons/{id}/edit")
public String updatePerson(@PathVariable Long id,
                          @Validated(ValidationGroups.Update.class) @ModelAttribute Person person,
                          BindingResult result) {
    // E-Mail ist optional!
}

5.8 Fehler im Template anzeigen

Erweitere person-form.html um Fehleranzeige:

<div class="form-group">
    <label for="firstname">Vorname: *</label>
    <input type="text" 
           id="firstname" 
           th:field="*{firstname}" 
           th:class="${#fields.hasErrors('firstname')} ? 'error' : ''"
           placeholder="z.B. Max"
           required />
    <span class="error-message" th:if="${#fields.hasErrors('firstname')}" 
          th:errors="*{firstname}"></span>
</div>

NEU: Telefon-Feld mit Custom Validator

<div class="form-group">
    <label for="phone">Telefon: (optional)</label>
    <input type="text" 
           id="phone" 
           th:field="*{phone}" 
           th:class="${#fields.hasErrors('phone')} ? 'error' : ''"
           placeholder="0151-12345678 oder +49-151-12345678" />
    <span class="error-message" th:if="${#fields.hasErrors('phone')}" 
          th:errors="*{phone}"></span>
</div>

Gleiches für person-edit-form.html!


5.9 CSS für Fehler

Ergänze in style.css:

.form-group input.error,
.form-group select.error,
.form-group textarea.error {
    border-color: #f44336;
    background-color: #ffebee;
}

.error-message {
    color: #f44336;
    font-size: 14px;
    margin-top: 5px;
    display: block;
    font-weight: 500;
}

.form-group input.error::placeholder {
    color: #e57373;
}

Schritt 6: Whitespace-Trimming

Problem: “ Max “ wird nicht getrimmt!

Lösung: @InitBinder

Erweitere PersonViewController:

import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;

@Controller
@RequiredArgsConstructor
public class PersonViewController {
    
    private final PersonService personService;
    
    // Automatisches Trimmen für ALLE Formulare!
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        StringTrimmerEditor stringTrimmer = new StringTrimmerEditor(true);
        binder.registerCustomEditor(String.class, stringTrimmer);
    }
    
    // ... rest der Methoden ...
}

Was macht das?

  • Trimmt automatisch alle String-Felder
  • true = Leere Strings werden zu null (wichtig für @NotBlank!)

Schritt 7: HTTP Error Pages

7.1 ErrorController erstellen

Erstelle: controller/CustomErrorController.java

package com.example.helloworldapi.controller;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;

@Controller
public class CustomErrorController implements ErrorController {
    
    @RequestMapping("/error")
    public String handleError(HttpServletRequest request, Model model) {
        Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        
        if (status != null) {
            int statusCode = Integer.parseInt(status.toString());
            
            if (statusCode == HttpStatus.NOT_FOUND.value()) {
                return "error/404";
            } else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
                return "error/500";
            }
        }
        
        return "error/generic";
    }
}

7.2 Error-Templates

Erstelle Ordner: templates/error/

Datei: templates/error/404.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>404 - Seite nicht gefunden</title>
    <link rel="stylesheet" th:href="@{/css/style.css}">
    <style>
        .error-container {
            max-width: 600px;
            margin: 100px auto;
            text-align: center;
            padding: 30px;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .error-code {
            font-size: 72px;
            font-weight: bold;
            color: #d32f2f;
            margin: 0;
        }
        .error-message {
            font-size: 24px;
            color: #666;
            margin: 20px 0;
        }
    </style>
</head>
<body>
    <div class="error-container">
        <h1 class="error-code">404</h1>
        <p class="error-message">Seite nicht gefunden</p>
        <p>Die von dir gesuchte Person oder Seite existiert nicht.</p>
        <a th:href="@{/persons}" class="btn-primary">Zurück zur Personen-Liste</a>
    </div>
</body>
</html>

Datei: templates/error/500.html (analog mit „500“ und „Server-Fehler“)


7.3 PersonViewController anpassen

Erweitere showPersonDetails:

import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;

@GetMapping("/persons/{id}")
public String showPersonDetails(@PathVariable Long id, Model model) {
    Person person = personService.getPersonById(id);
    
    if (person == null) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Person nicht gefunden");
    }
    
    model.addAttribute("person", person);
    return "person-details";
}

🔵 BONUS: EIGENES PROJEKT

Schritt 8: Praktische Aufgabe – Buchbewertungs-System

Jetzt bist du dran! Wende alles Gelernte an.

Aufgabenstellung

Baue ein Buchbewertungs-System mit:

Datenmodell:

public class BookReview {
    private Long id;
    private String bookTitle;      // Buchtitel
    private String author;         // Autor
    private Integer rating;        // 1-5 Sterne
    private String reviewText;     // Bewertungstext
    private LocalDate reviewDate;  // Bewertungsdatum
}

Funktionen:

  • ✅ Liste aller Bewertungen
  • ✅ Details einer Bewertung
  • ✅ Neue Bewertung (Formular)
  • ✅ Bewertung bearbeiten
  • ✅ Bewertung löschen

Validation:

  • Buchtitel: 2-100 Zeichen, Pflicht, nur Buchstaben/Zahlen/Satzzeichen
  • Autor: 2-50 Zeichen, Pflicht
  • Rating: 1-5, Pflicht (Integer!)
  • Bewertungstext: 10-500 Zeichen, Pflicht
  • Datum: Automatisch (heute)

Starter-Code

BookReview.java

package com.example.helloworldapi.model;

import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookReview {
    
    private Long id;
    
    @NotBlank(message = "Buchtitel darf nicht leer sein")
    @Size(min = 2, max = 100, message = "Buchtitel: 2-100 Zeichen")
    @Pattern(regexp = "^[a-zA-Z0-9äöüÄÖÜß\\s\\-:,.!?]+$",
             message = "Buchtitel: nur Buchstaben, Zahlen und Satzzeichen")
    private String bookTitle;
    
    @NotBlank(message = "Autor darf nicht leer sein")
    @Size(min = 2, max = 50, message = "Autor: 2-50 Zeichen")
    @Pattern(regexp = "^[a-zA-ZäöüÄÖÜß\\s\\-\\.]+$",
             message = "Autor: nur Buchstaben")
    private String author;
    
    @NotNull(message = "Rating darf nicht leer sein")
    @Min(value = 1, message = "Minimum: 1 Stern")
    @Max(value = 5, message = "Maximum: 5 Sterne")
    private Integer rating;
    
    @NotBlank(message = "Bewertungstext darf nicht leer sein")
    @Size(min = 10, max = 500, message = "Bewertungstext: 10-500 Zeichen")
    private String reviewText;
    
    private LocalDate reviewDate;
}

BookReviewService.java

package com.example.helloworldapi.service;

import com.example.helloworldapi.model.BookReview;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Service
public class BookReviewService {
    
    private final List<BookReview> reviews = new ArrayList<>();
    private long nextId = 1;
    
    public BookReviewService() {
        reviews.add(new BookReview(
            nextId++,
            "Clean Code",
            "Robert C. Martin",
            5,
            "Ein absolutes Muss für jeden Entwickler!",
            LocalDate.now().minusDays(10)
        ));
    }
    
    // TODO: Implementiere CRUD-Methoden
    // getAllReviews()
    // getReviewById(Long id)
    // createReview(BookReview review)
    // updateReview(Long id, BookReview review)
    // deleteReview(Long id)
}

Checkliste

Model & Service:

  • [ ] BookReview.java mit Validation
  • [ ] BookReviewService.java mit CRUD

Controller:

  • [ ] BookReviewViewController.java
  • [ ] Alle CRUD-Endpoints
  • [ ] @InitBinder für Whitespace

Templates:

  • [ ] reviews-list.html
  • [ ] review-details.html
  • [ ] review-form.html
  • [ ] Error-Pages

Features:

  • [ ] Validation funktioniert
  • [ ] Error-Handling (404/500)
  • [ ] Whitespace-Trimming
  • [ ] Sterne-Anzeige (⭐⭐⭐⭐⭐)

Tipps

Rating-Select:

<div class="form-group">
    <label for="rating">Rating (1-5 Sterne): *</label>
    <select id="rating" th:field="*{rating}" required>
        <option value="">-- Bitte wählen --</option>
        <option value="1">⭐ (1 Stern)</option>
        <option value="2">⭐⭐ (2 Sterne)</option>
        <option value="3">⭐⭐⭐ (3 Sterne)</option>
        <option value="4">⭐⭐⭐⭐ (4 Sterne)</option>
        <option value="5">⭐⭐⭐⭐⭐ (5 Sterne)</option>
    </select>
</div>

Textarea für Bewertungstext:

<div class="form-group">
    <label for="reviewText">Bewertungstext: *</label>
    <textarea id="reviewText" 
              th:field="*{reviewText}"
              rows="5"
              placeholder="Schreibe deine Bewertung..."
              maxlength="500"></textarea>
    <div class="char-count">
        <span id="char-count">0</span> / 500 Zeichen
    </div>
</div>

Datum automatisch setzen:

public BookReview createReview(BookReview review) {
    review.setId(nextId++);
    review.setReviewDate(LocalDate.now());  // Automatisch!
    reviews.add(review);
    return review;
}

Sterne in der Liste:

<td>
    <span th:each="star : ${#numbers.sequence(1, review.rating)}">⭐</span>
    (<span th:text="${review.rating}">5</span>/5)
</td>

🧪 Kompletter Test-Durchlauf

Teste jetzt alles:

1. Basis-CRUD

  • Neue Person: http://localhost:8080/persons/new
  • Person bearbeiten: http://localhost:8080/persons/42/edit
  • Person löschen: Klick auf „Löschen“

2. Validation

  • Leeres Formular → Alle Fehler werden angezeigt
  • Zu kurzer Name („M“) → Size-Fehler
  • Name mit Zahlen („Max123“) → Pattern-Fehler
  • Ungültige E-Mail („max@“) → Email-Fehler
  • Ungültige Telefonnummer („123“) → Custom Validator-Fehler
  • Gültige Telefonnummer („+49-151-12345678“) → Funktioniert!

3. Error-Handling

  • Nicht existierende ID: http://localhost:8080/persons/999 → 404-Seite

4. Whitespace

  • “ Max “ eingeben → wird automatisch zu „Max“
  • “ “ eingeben → @NotBlank greift (null)

5. Validation Groups (wenn implementiert)

  • CREATE: E-Mail Pflicht
  • UPDATE: E-Mail optional

✅ Checkpoint: Hast du Tag 4 geschafft?

Kontrolliere:

  • [ ] Formular zum Erstellen (GET + POST /persons/new)
  • [ ] Formular zum Bearbeiten (GET + POST /persons/{id}/edit)
  • [ ] Delete-Button (POST /persons/{id}/delete)
  • [ ] Validation: @NotBlank, @Size, @Email, @Pattern, Custom Validator
  • [ ] Validation Groups: Create vs Update
  • [ ] Error-Messages: Schön im Template
  • [ ] Whitespace-Trimming: @InitBinder
  • [ ] Error-Pages: 404/500
  • [ ] Buchbewertungs-Aufgabe: Vollständig
  • [ ] Service-Wiederverwendung: PersonService aus Tag 2
  • [ ] CRUD komplett: REST API + HTML-Interface

Alles ✅? Du bist bereit für Tag 5!

Probleme?

  • Validation? → Schritt 5.5 (BindingResult)
  • Custom Validator? → Schritt 5.4
  • Whitespace? → Schritt 6
  • 404-Seite? → Schritt 7.2
  • Download das Projekt unten

Hier ein paar gute externe Links zum Thema Thymeleaf Forms & das Spring MVC-Pattern die du vielleicht auch beim lernen Berücksichtigen solltest:

  • „Handling Form Submission” – offizielle Guide von Spring Framework: eine Schritt-für-Schritt Anleitung für Webformulare in Spring MVC. Home
  • „Spring Boot Thymeleaf Form Handling Tutorial” – praxisnah bei CodeJava: zeigt, wie du mit Thymeleaf Formulare mit allen gängigen HTML-Feldtypen erstellst. codejava.net
  • „Introduction to Using Thymeleaf in Spring” – bei Baeldung: erklärt Grundlage der Thymeleaf-Integration mit Spring MVC. Baeldung on Kotlin
  • „Getting Started with Forms in Spring MVC | Baeldung” – fokussiert auf Formulare & Datenbindung (ModelAttribute etc.). Baeldung on Kotlin
  • „Form handling with Thymeleaf” – Blogpost von Wim Deblauwe: detaillierter Ablauf von GET-Formular, POST, Validierung mit Thymeleaf & Spring Boot. wimdeblauwe.com

🔥 Elyndras Real Talk:

Marcus kam heute Morgen rein: „Also jetzt haben wir das gleiche CRUD zweimal – einmal als REST API, einmal als HTML. Ist das nicht… doppelte Arbeit?“

Meine Antwort: „Nein Marcus, das ist Best Practice!

Ich hab letztes Jahr bei AutoTech ein System gebaut:

  • REST API für die Mobile App (React Native)
  • HTML-Interface für das Admin-Dashboard (Thymeleaf)
  • Ein Service für beide!

Warum beide?

REST API: Mobile Apps, Frontend-Teams, Partner, Maschinenkommunikation
HTML-Interface: Admin-Tools, interne Mitarbeiter, SEO, Reporting

Das Schöne:

@Service
public class PersonService {
    // EINE Business-Logik
    // Beide Controller nutzen sie
    // Änderung hier → beide profitieren!
}

Marcus‘ Fazit: „Ah! Ein Werkzeug, zwei Griffe – clever!“

Und das Beste: Mit Validation, Custom Validators und Error-Pages ist dein Code jetzt Production-Ready! 🎯

Eomma übrigens liebt die HTML-Formulare: „Endlich kann ich sehen was passiert!“ Und die Validation-Error-Messages versteht sogar sie: „Vorname muss zwischen 2 und 50 Zeichen sein“ – klarer geht’s nicht!


❓ FAQ (Häufige Fragen)

Q: Warum @NotBlank statt @NotNull für Strings?
A: @NotNull erlaubt leere Strings („“), @NotBlank nicht! Für Formulare fast immer @NotBlank.

Q: Was ist der Unterschied zwischen @Size und @Length?
A: @Size ist Standard (jakarta.validation), @Length ist Hibernate-spezifisch. Nutze @Size!

Q: Warum Custom Validators statt nur @Pattern?
A: Custom Validators sind wiederverwendbar, testbar und haben sauberere Error-Messages.

Q: Müssen Validation Groups immer verwendet werden?
A: Nein! Nur wenn unterschiedliche Regeln für Create vs Update nötig sind.

Q: Wie validiere ich verschachtelte Objekte?
A: Mit @Valid auch für das verschachtelte Feld: @Valid private Address address;

Q: Warum StringTrimmerEditor(true) statt (false)?
A: true = leere Strings werden zu null → @NotBlank greift! false = leere Strings bleiben leer.

Q: Kann ich eigene Error-Messages mit Variablen haben?
A: Ja! message = "Wert muss zwischen {min} und {max} sein" – Spring ersetzt {min} und {max} automatisch!

Q: Was macht ihr bei persönlichen Problemen zwischen den Projekten?
A: Das ist… kompliziert. Manche Geschichten gehören nicht in Tech-Blogs, sondern in private logs. Aber das ist ein anderes Kapitel. 🔒


📅 Nächster Kurstag: Tag 5

Morgen im Kurs / Nächster Blogbeitrag:

„Konfiguration & Logging – application.properties meistern“

Was du lernen wirst:

  • application.properties richtig nutzen
  • Profile (dev, prod) verstehen
  • Logging konfigurieren (Logback)
  • Environment-spezifische Konfiguration
  • @Value und @ConfigurationProperties

Warum wichtig? Echte Apps brauchen verschiedene Konfigurationen für verschiedene Umgebungen!

Voraussetzung: Tag 4 abgeschlossen

👉 Zum Blogbeitrag Tag 5 (erscheint morgen)


📚 Deine Fortschritts-Übersicht

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

Du hast 40% des Kurses geschafft! 💪

Alle Blogbeiträge dieser Serie:
👉 Spring Boot Basic – Komplette Übersicht


📥 Download & Ressourcen

Projekt zum Download:
👉 SpringBootDay4-Forms-v1.0.zip (Stand: 19.10.2025)

Was ist im ZIP enthalten:

  • ✅ Komplettes Maven-Projekt
  • ✅ PersonViewController mit CRUD
  • ✅ PersonController (REST API) aus Tag 2
  • ✅ PersonService (wiederverwendet!)
  • ✅ Alle Templates (Forms, Details, Error-Pages)
  • ✅ CSS komplett
  • Validation komplett: Standard + Custom Validators
  • Validation Groups: Create vs Update
  • Custom Error Pages: 404, 500
  • Whitespace-Trimming: @InitBinder
  • Buchbewertungs-System: Vollständige Lösung
  • ✅ README mit Schnellstart

Projekt starten:

# ZIP entpacken
# In NetBeans öffnen: File → Open Project
# App starten wie gewohnt

Probleme? Issue melden oder schreib mir: elyndra@java-developer.online


Das war Tag 4 von Spring Boot Basic!

Du kannst jetzt:

  • ✅ HTML-Formulare mit Thymeleaf erstellen
  • ✅ th:object, th:field, th:action nutzen
  • ✅ @PostMapping für Formulare
  • ✅ CRUD komplett (CREATE, UPDATE, DELETE)
  • Production-Ready Validation: @NotBlank, @Size, @Email, @Pattern, @Min, @Max
  • Custom Validators: Eigene Annotations & Validator-Klassen
  • Validation Groups: Verschiedene Regeln für Create vs Update
  • Schöne Error-Messages: ControllerAdvice & Template-Feedback
  • Whitespace-Trimming: @InitBinder
  • Professional Error-Handling: Custom 404/500 Pages
  • CRUD komplett: REST API + HTML-Interface!
  • Service-Wiederverwendung: Ein Service, zwei Interfaces!
  • MVC-Pattern beherrschen!
  • Eigenes Projekt: Buchbewertungs-System von Grund auf!

Morgen lernst du Konfiguration & Logging – für echte Production-Apps! 🚀

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 5 erscheint morgen. Bis dahin: Happy Coding!


Tags: #SpringBoot #Thymeleaf #Forms #MVC #CRUD #Validation #Tag4

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.