Spring Boot Basic – Tag 4 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 | Abgeschlossen |
| → 4 | Thymeleaf Forms & MVC-Pattern | 👉 DU BIST HIER! |
| 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: 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-Objektth: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 ValidierungBindingResult→ 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 zunull(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
| Tag | Thema | Status |
|---|---|---|
| ✅ 1 | Erste REST API | ABGESCHLOSSEN! 🎉 |
| ✅ 2 | Spring Container & DI + CRUD | ABGESCHLOSSEN! 🎉 |
| ✅ 3 | @Controller & Thymeleaf Basics | ABGESCHLOSSEN! 🎉 |
| ✅ 4 | Thymeleaf Forms & MVC-Pattern | ABGESCHLOSSEN! 🎉 |
| → 5 | Konfiguration & Logging | Als nächstes |
| 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 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

