Woche 4, Tag 4 — Exception Handling in Spring MVC
Ziel
Heute verstehst du Exception Handling in Spring MVC REST APIs.
Die Kernfragen:
- Was passiert, wenn ein Controller eine Exception wirft?
- Warum brauchen REST APIs saubere Fehlerantworten?
- Was ist
@ExceptionHandler? - Was ist
@ControllerAdvice? - Was ist
@RestControllerAdvice? - Was ist
@ResponseStatus? - Was ist
ResponseStatusException? - Was ist
ProblemDetail? - Was ist
ResponseEntityExceptionHandler? - Wie behandelst du Validierungsfehler?
- Welche HTTP-Statuscodes sind bei Fehlern üblich?
- Welche typischen Prüfungsfallen gibt es?
1. Kurz-Wiederholung aus Woche 4, Tag 3
An Tag 3 hast du gelernt:
@RequestBodyliest den HTTP-Body in ein Java-Objekt.@ResponseBodyschreibt einen Java-Rückgabewert in den HTTP-Response-Body.@RestControllerenthält@ResponseBody.HttpMessageConverterkonvertiert zwischen HTTP-Bodies und Java-Objekten.- Jackson konvertiert üblicherweise JSON zu Java und Java zu JSON.
- DTOs definieren den API-Vertrag.
@Validlöst die Validierung des Request-Bodys aus.ResponseEntitysteuert Status, Header und Body.
Merksatz:
Request body kommt vom Client.
Response body geht zurück an den Client.
Heute lernst du, was passiert, wenn etwas schiefgeht.
2. Warum Exception Handling wichtig ist
In echten APIs kann vieles schiefgehen:
resource not found
validation error
invalid JSON
missing request parameter
wrong path variable type
database conflict
unauthorized request
forbidden action
business rule violation
unexpected server error
Ohne gutes Exception Handling kann die API zurückgeben:
ugly stack traces
unclear messages
wrong status codes
inconsistent error formats
sensitive internal details
Eine gute API sollte zurückgeben:
clear HTTP status
stable error body
safe message
useful error code
validation field errors if needed
no internal stack trace
Merksatz:
Gutes Exception Handling macht API-Fehler vorhersehbar und sicher.
3. Was passiert, wenn eine Exception geworfen wird?
Beispiel:
@GetMapping("/api/tasks/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}
Service:
public TaskDto findById(Long id) {
throw new TaskNotFoundException(id);
}
Wenn niemand die Exception behandelt, versucht Spring MVC, sie über seinen Exception-Handling-Mechanismus aufzulösen.
Mögliches Ergebnis:
500 Internal Server Error
oder ein anderer Status — je nach Exception-Typ.
Problem:
Der Client bekommt möglicherweise keine saubere, hilfreiche Fehlerantwort.
4. Schlechter Stil: Try-Catch in jeder Controller-Methode
Schlecht:
@GetMapping("/{id}")
public ResponseEntity<?> getTask(@PathVariable Long id) {
try {
TaskDto task = taskService.findById(id);
return ResponseEntity.ok(task);
} catch (TaskNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body("Task not found");
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Something went wrong");
}
}
Warum schlecht?
repeated code
controllers become noisy
error response format becomes inconsistent
hard to maintain
easy to forget cases
business code mixed with error handling
Besser:
Throw meaningful exceptions.
Handle them centrally.
5. Besserer Stil: Domain Exceptions werfen
Beispiel für eine eigene Exception:
public class TaskNotFoundException extends RuntimeException {
public TaskNotFoundException(Long id) {
super("Task not found with id " + id);
}
}
Service:
@Service
public class TaskService {
public TaskDto findById(Long id) {
return taskRepository.findById(id)
.map(this::toDto)
.orElseThrow(() -> new TaskNotFoundException(id));
}
}
Controller:
@GetMapping("/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}
Der Controller bleibt sauber.
Exception Handling passiert woanders.
Merksatz:
Services können sinnvolle Exceptions werfen. Controller sollten dünn bleiben.
6. @ExceptionHandler
@ExceptionHandler markiert eine Methode, die Exceptions behandelt.
Beispiel innerhalb eines Controllers:
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@GetMapping("/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handleTaskNotFound(TaskNotFoundException ex) {
ErrorResponseDto error = new ErrorResponseDto(
"TASK_NOT_FOUND",
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
Bedeutung:
Wenn in diesem Controller eine TaskNotFoundException geworfen wird,
behandle sie mit handleTaskNotFound().
7. Lokaler @ExceptionHandler
Wenn @ExceptionHandler in einem Controller liegt, behandelt er Exceptions für diesen Controller.
Beispiel:
@RestController
public class TaskController {
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handle(TaskNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponseDto("TASK_NOT_FOUND", ex.getMessage()));
}
}
Geltungsbereich:
nur TaskController
Das ist manchmal nützlich — für REST APIs bevorzugen wir aber meist globales Handling.
8. Globales Exception Handling mit @ControllerAdvice
@ControllerAdvice wird für globale controller-bezogene Logik genutzt.
Für Exceptions:
@ControllerAdvicelässt Exception Handler über viele Controller hinweg gelten.
Beispiel:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handleTaskNotFound(TaskNotFoundException ex) {
ErrorResponseDto error = new ErrorResponseDto(
"TASK_NOT_FOUND",
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
Jetzt kann der Handler Exceptions aus mehreren Controllern behandeln.
Merksatz:
@ControllerAdviceist für controller-übergreifendes Exception Handling.
9. @RestControllerAdvice
Für REST APIs nutze:
@RestControllerAdvice
Sehr wichtig:
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
Das heißt: Rückgabewerte des Handlers werden in den Response-Body geschrieben.
Beispiel:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handleTaskNotFound(TaskNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponseDto("TASK_NOT_FOUND", ex.getMessage()));
}
}
Für REST APIs ist das meist sauberer als einfaches @ControllerAdvice.
10. Error-Response-DTO
Eine einfache eigene Fehlerantwort:
public record ErrorResponseDto(
String code,
String message
) {
}
Beispielantwort:
{
"code": "TASK_NOT_FOUND",
"message": "Task not found with id 123"
}
Besser mit mehr Feldern:
public record ApiErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path
) {
}
Beispiel:
{
"timestamp": "2026-07-07T18:30:00Z",
"status": 404,
"error": "Not Found",
"code": "TASK_NOT_FOUND",
"message": "Task not found with id 123",
"path": "/api/tasks/123"
}
Merksatz:
Fehlerantworten sollten eine stabile Struktur haben.
11. Globaler Handler mit eigenem Error-Body
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleTaskNotFound(
TaskNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
"TASK_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
Das liefert:
HTTP 404 Not Found
mit einem JSON-Error-Body.
12. Wichtig: Keine internen Details preisgeben
Schlecht:
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle(Exception ex) {
return ResponseEntity.internalServerError()
.body(ex.toString());
}
Warum schlecht?
Das kann preisgeben:
class names
SQL details
internal package names
stack traces
security information
database structure
tokens or secrets
Besser:
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
500,
"Internal Server Error",
"INTERNAL_ERROR",
"An unexpected error occurred",
request.getRequestURI()
);
return ResponseEntity.internalServerError().body(error);
}
Logge die echte Exception intern — gib sie aber nicht an den Client weiter.
Merksatz:
Interne Details loggen. Sichere Meldungen zurückgeben.
13. @ResponseStatus auf der Exception-Klasse
Ein anderer Weg, eine Exception einem Status zuzuordnen:
@ResponseStatus(HttpStatus.NOT_FOUND)
public class TaskNotFoundException extends RuntimeException {
public TaskNotFoundException(Long id) {
super("Task not found with id " + id);
}
}
Wenn diese Exception geworfen wird, kann Spring zurückgeben:
404 Not Found
Einfach und nützlich.
Aber Einschränkung:
Weniger Kontrolle über den eigenen Response-Body.
Für produktive REST APIs gibt @ControllerAdvice oft mehr Kontrolle.
14. @ResponseStatus auf der Handler-Methode
Beispiel:
@PostMapping("/api/tasks")
@ResponseStatus(HttpStatus.CREATED)
public TaskDto create(@Valid @RequestBody CreateTaskRequest request) {
return taskService.create(request);
}
Das gilt nicht nur für Exceptions.
Es kann auch einen festen Erfolgs-Status setzen.
Merksatz:
@ResponseStatussetzt einen festen HTTP-Status.
15. ResponseStatusException
ResponseStatusException ist nützlich für einfache Fälle.
Beispiel:
@GetMapping("/api/tasks/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Task not found"
));
}
Das liefert:
404 Not Found
Nutze es für schnelle/einfache Fälle.
Für größere Apps sind eigene Exceptions plus globales Handling meist sauberer.
16. ResponseStatusException vs. eigene Exception
| Thema | ResponseStatusException | Eigene Exception + Advice |
|---|---|---|
| Geschwindigkeit | schnell | mehr Setup |
| Wiederverwendung | weniger domain-spezifisch | wiederverwendbar |
| Error body | weniger strukturiert ohne Anpassung | strukturiert |
| Domain-Bedeutung | schwächer | stärker |
| Große App | weniger ideal | besser |
Merksatz:
ResponseStatusExceptionist schnell. Eigene Exceptions plus Advice sind für größere APIs sauberer.
17. ProblemDetail
Modernes Spring unterstützt ProblemDetail.
Einfache Definition:
ProblemDetailist eine Standardstruktur für HTTP-API-Fehlerantworten.
Beispiel:
@ExceptionHandler(TaskNotFoundException.class)
public ProblemDetail handleTaskNotFound(TaskNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
problem.setTitle("Task not found");
problem.setDetail(ex.getMessage());
problem.setProperty("code", "TASK_NOT_FOUND");
return problem;
}
Beispiel für die Antwortstruktur:
{
"type": "about:blank",
"title": "Task not found",
"status": 404,
"detail": "Task not found with id 123",
"instance": "/api/tasks/123",
"code": "TASK_NOT_FOUND"
}
Merksatz:
ProblemDetailliefert ein standardisiertes Fehlerantwort-Format.
18. Eigenes DTO vs. ProblemDetail
| Thema | Eigenes DTO | ProblemDetail |
|---|---|---|
| Format | vollständig eigen | Standardformat |
| Felder | beliebige Felder | Standardfelder plus eigene Properties |
| Gut für | firmenspezifischer API-Stil | standardisierte HTTP-API-Fehler |
| Modernes Spring | ja | ja |
| Flexibilität | hoch | hoch, mit Properties |
Beides ist gültig.
Für die Zertifizierung kenne das Konzept:
Spring MVC kann Exceptions mit @ExceptionHandler behandeln und ResponseEntity, eigene DTOs oder ProblemDetail zurückgeben.
19. Validierungsfehler
Aus Tag 3:
public record CreateTaskRequest(
@NotBlank
String title,
@NotBlank
String priority
) {
}
Controller:
@PostMapping("/api/tasks")
public TaskDto create(@Valid @RequestBody CreateTaskRequest request) {
return taskService.create(request);
}
Ungültiger Request:
{
"title": "",
"priority": ""
}
Die Validierung schlägt fehl, bevor der Controller-Body ausgeführt wird.
Übliche Antwort:
400 Bad Request
Aber die Standardantwort hat vielleicht nicht das Format, das du willst.
Also passen wir sie an.
20. Häufige Validierungs-Exception
Bei ungültigem @Valid @RequestBody wirft Spring MVC üblicherweise:
MethodArgumentNotValidException
Darin stecken Feldfehler.
Beispiel-Feldfehler:
title must not be blank
priority must not be blank
Du kannst das mit @ExceptionHandler behandeln.
21. Validierungsfehler-DTO
Erstelle DTOs:
public record ValidationErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path,
List<FieldErrorDto> fieldErrors
) {
}
public record FieldErrorDto(
String field,
String message
) {
}
Beispielantwort:
{
"timestamp": "2026-07-07T18:30:00Z",
"status": 400,
"error": "Bad Request",
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"path": "/api/tasks",
"fieldErrors": [
{
"field": "title",
"message": "must not be blank"
},
{
"field": "priority",
"message": "must not be blank"
}
]
}
22. Validierungsfehler behandeln
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationError(
MethodArgumentNotValidException ex,
HttpServletRequest request
) {
List<FieldErrorDto> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new FieldErrorDto(
error.getField(),
error.getDefaultMessage()
))
.toList();
ValidationErrorResponse response = new ValidationErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"VALIDATION_ERROR",
"Request validation failed",
request.getRequestURI(),
fieldErrors
);
return ResponseEntity.badRequest().body(response);
}
}
Merksatz:
MethodArgumentNotValidExceptionist wichtig für Request-Body-Validierungsfehler.
23. Validierungsfehler bei Methodenparametern
Beispiel:
@RestController
@Validated
@RequestMapping("/api/tasks")
public class TaskController {
@GetMapping
public List<TaskDto> list(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
) {
return taskService.findPage(page, size);
}
}
Ungültiger Request:
GET /api/tasks?page=-1&size=500
Das ist keine Request-Body-Validierung.
Das ist Methodenparameter-Validierung.
Je nach Spring-Version und Setup kann der Exception-Typ unterschiedlich sein.
Prüfungssicherer Merksatz:
@RequestBody-Validierung löst üblicherweise MethodArgumentNotValidException aus.
Request-Parameter-Validierung nutzt Method Validation und kann eine Constraint Violation oder Method Validation Exception erzeugen.
24. Fehlender Request-Parameter
Controller:
@GetMapping("/api/tasks")
public List<TaskDto> list(@RequestParam String status) {
return taskService.findByStatus(status);
}
Request:
GET /api/tasks
Weil status standardmäßig erforderlich ist, liefert Spring:
400 Bad Request
Das ist nicht dasselbe wie Bean Validation.
Das ist ein Spring-MVC-Binding-/Request-Fehler.
25. Typkonvertierungsfehler
Controller:
@GetMapping("/api/tasks/{id}")
public TaskDto getTask(@PathVariable Long id) {
return taskService.findById(id);
}
Request:
GET /api/tasks/abc
Spring kann nicht konvertieren:
abc -> Long
Übliche Antwort:
400 Bad Request
Auch das ist ein Binding-/Typkonvertierungsfehler.
26. Ungültiger JSON-Fehler
Request:
POST /api/tasks
Content-Type: application/json
{
"title":
}
Spring kann den JSON-Body nicht parsen.
Übliche Antwort:
400 Bad Request
Häufige Exception-Kategorie:
message not readable
In Spring MVC wird das üblicherweise repräsentiert durch:
HttpMessageNotReadableException
27. Ungültiges JSON behandeln
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiErrorResponse> handleInvalidJson(
HttpMessageNotReadableException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"INVALID_JSON",
"Request body is missing or invalid",
request.getRequestURI()
);
return ResponseEntity.badRequest().body(error);
}
Wichtig:
Gib die rohe Parser-Exception nicht an den Client zurück.
Gib eine sichere Meldung zurück.
28. Business-Rule-Fehler
Beispiel:
public class TaskAlreadyCompletedException extends RuntimeException {
public TaskAlreadyCompletedException(Long id) {
super("Task is already completed: " + id);
}
}
Das ist kein Validierungs-Annotations-Problem.
Das ist ein Business-Rule-Problem.
Möglicher Status:
409 Conflict
Handler:
@ExceptionHandler(TaskAlreadyCompletedException.class)
public ResponseEntity<ApiErrorResponse> handleTaskAlreadyCompleted(
TaskAlreadyCompletedException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.CONFLICT.value(),
HttpStatus.CONFLICT.getReasonPhrase(),
"TASK_ALREADY_COMPLETED",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
Merksatz:
Nutze 409 Conflict, wenn der Request mit dem aktuellen Ressourcen-Zustand kollidiert.
29. Not-Found-Fehler
Exception:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Object id) {
super(resource + " not found with id " + id);
}
}
Handler:
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
"RESOURCE_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
Status:
404 Not Found
30. Unauthorized vs. Forbidden
Das ist eine häufige Interview- und Prüfungsfalle.
401 Unauthorized
Bedeutet:
Der Client ist nicht authentifiziert oder die Authentifizierung ist fehlgeschlagen.
Beispiel:
fehlender Token
ungültiger Token
abgelaufener Token
403 Forbidden
Bedeutet:
Der Client ist authentifiziert, darf diese Aktion aber nicht ausführen.
Beispiel:
normaler Nutzer versucht Admin-Endpoint aufzurufen
Nutzer versucht auf Daten eines anderen Tenants zuzugreifen
Merksatz:
401 = Wer bist du? 403 = Du bist bekannt, aber nicht berechtigt.
31. Häufige Fehler-Statuscodes
| Situation | Status |
|---|---|
| Validierungsfehler | 400 Bad Request |
| Ungültiges JSON | 400 Bad Request |
| Fehlender Request-Parameter | 400 Bad Request |
| Path-Variable-Konvertierungsfehler | 400 Bad Request |
| Authentifizierung fehlt oder ungültig | 401 Unauthorized |
| Authentifiziert, aber nicht berechtigt | 403 Forbidden |
| Ressource nicht gefunden | 404 Not Found |
| Falsche HTTP-Methode | 405 Method Not Allowed |
| Business-Konflikt | 409 Conflict |
| Nicht unterstützter Request-Body-Typ | 415 Unsupported Media Type |
| Unerwarteter Serverfehler | 500 Internal Server Error |
32. ResponseEntityExceptionHandler
Spring stellt ResponseEntityExceptionHandler bereit.
Einfache Definition:
ResponseEntityExceptionHandlerist eine praktische Basisklasse für viele eingebaute Spring-MVC-Exceptions.
Beispiel:
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleTaskNotFound(
TaskNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
"TASK_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
Das ist nützlich, wenn du das eingebaute Exception Handling von Spring MVC anpassen willst.
Für die Prüfung:
Kenne, dass es existiert.
Kenne, dass es mit @ControllerAdvice / @RestControllerAdvice genutzt wird.
Kenne, dass es beim Behandeln eingebauter MVC-Exceptions hilft.
33. Auflösung von Exception Handlern
Wenn eine Exception auftritt, sucht Spring nach einem passenden Handler.
Vereinfachte Reihenfolge:
1. Suche @ExceptionHandler im Controller.
2. Suche @ExceptionHandler in @ControllerAdvice / @RestControllerAdvice.
3. Nutze eingebautes Spring-MVC-Exception-Handling.
4. Wenn unaufgelöst: generischen Serverfehler zurückgeben.
Wichtig:
Ein spezifischerer Exception Handler hat Vorrang vor einem generischen.
Beispiel:
@ExceptionHandler(TaskNotFoundException.class)
ist spezifischer als:
@ExceptionHandler(Exception.class)
34. Generischer Exception Handler
Ein generischer Handler ist als letzter Fallback nützlich.
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
"INTERNAL_ERROR",
"An unexpected error occurred",
request.getRequestURI()
);
return ResponseEntity.internalServerError().body(error);
}
Wichtig:
Verschlucke nicht alle Fehler still.
Logge die Exception.
Gib eine sichere Antwort zurück.
Beispiel mit Logging:
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
log.error("Unexpected error", ex);
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
500,
"Internal Server Error",
"INTERNAL_ERROR",
"An unexpected error occurred",
request.getRequestURI()
);
return ResponseEntity.internalServerError().body(error);
}
35. Vollständiges Beispiel für globalen Exception Handler
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
"RESOURCE_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(TaskAlreadyCompletedException.class)
public ResponseEntity<ApiErrorResponse> handleConflict(
TaskAlreadyCompletedException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.CONFLICT.value(),
HttpStatus.CONFLICT.getReasonPhrase(),
"TASK_ALREADY_COMPLETED",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(
MethodArgumentNotValidException ex,
HttpServletRequest request
) {
List<FieldErrorDto> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new FieldErrorDto(
error.getField(),
error.getDefaultMessage()
))
.toList();
ValidationErrorResponse response = new ValidationErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"VALIDATION_ERROR",
"Request validation failed",
request.getRequestURI(),
fieldErrors
);
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiErrorResponse> handleInvalidJson(
HttpMessageNotReadableException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
"INVALID_JSON",
"Request body is missing or invalid",
request.getRequestURI()
);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleUnexpected(
Exception ex,
HttpServletRequest request
) {
log.error("Unexpected error", ex);
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
"INTERNAL_ERROR",
"An unexpected error occurred",
request.getRequestURI()
);
return ResponseEntity.internalServerError().body(error);
}
}
DTOs:
public record ApiErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path
) {
}
public record ValidationErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path,
List<FieldErrorDto> fieldErrors
) {
}
public record FieldErrorDto(
String field,
String message
) {
}
36. Ablauf des Exception Handlings
Beispiel-Request:
GET /api/tasks/999
Ablauf:
1. Request erreicht TaskController.
2. Controller ruft TaskService auf.
3. TaskService findet den Task nicht.
4. TaskService wirft ResourceNotFoundException.
5. DispatcherServlet fängt die Exception.
6. Spring sucht einen @ExceptionHandler.
7. GlobalExceptionHandler.handleNotFound() behandelt sie.
8. Error-DTO wird zurückgegeben.
9. Jackson konvertiert Error-DTO zu JSON.
10. Client erhält 404-Antwort.
Merksatz:
Exception -> Handler -> Error DTO -> JSON-Antwort.
37. Sauberer Controller mit globalem Exception Handling
Controller:
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping("/{id}")
public TaskDto get(@PathVariable Long id) {
return taskService.findById(id);
}
@PostMapping
public ResponseEntity<TaskDto> create(
@Valid @RequestBody CreateTaskRequest request
) {
TaskDto created = taskService.create(request);
URI location = URI.create("/api/tasks/" + created.id());
return ResponseEntity.created(location).body(created);
}
}
Kein try-catch.
Warum?
Bekannte Exceptions werden global behandelt.
Validierungsfehler werden global behandelt.
Unerwartete Fehler werden sicher behandelt.
Das ist sauberes REST-API-Design.
38. Gutes Error-Response-Design
Eine gute Fehlerantwort sollte sein:
consistent
safe
machine-readable
human-readable
easy to debug
not too verbose
not leaking internals
Nützliche Felder:
timestamp
status
error
code
message
path
fieldErrors
traceId
Beispiel:
{
"timestamp": "2026-07-07T18:30:00Z",
"status": 404,
"error": "Not Found",
"code": "TASK_NOT_FOUND",
"message": "Task not found with id 123",
"path": "/api/tasks/123"
}
39. Trace ID in Fehlerantworten
In Produktion ist eine Trace ID nützlich.
Beispiel:
{
"status": 500,
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"traceId": "abc-123"
}
Warum nützlich?
Client meldet traceId.
Backend-Team sucht Logs per traceId.
Problem ist leichter zu debuggen.
Das ist besonders in verteilten Systemen nützlich.
40. Was nicht in Fehlerantworten gehört
Vermeide, preiszugeben:
stack traces
SQL queries
database table names
internal class names
file paths
tokens
passwords
personal data
secret keys
full exception dumps
Gib sichere Meldungen zurück.
Logge detaillierte Fehler intern.
Merksatz:
Fehlerantworten sind für Clients. Logs sind für Entwickler.
41. Typische Prüfungsfallen
Falle 1
@ExceptionHandler behandelt Exceptions, die von Controller-Methoden geworfen werden.
Falle 2
Ein lokaler @ExceptionHandler in einem Controller gilt für diesen Controller.
Falle 3
@ControllerAdvice gilt global über Controller hinweg.
Falle 4
@RestControllerAdvice entspricht @ControllerAdvice plus @ResponseBody.
Falle 5
@ResponseStatus setzt einen festen HTTP-Status, gibt aber weniger Kontrolle über den Response-Body.
Falle 6
ResponseStatusException ist nützlich für schnelle/einfache HTTP-Fehler.
Falle 7
MethodArgumentNotValidException wird üblicherweise bei fehlgeschlagenem @Valid @RequestBody genutzt.
Falle 8
Ungültiges JSON bedeutet üblicherweise 400 Bad Request.
Falle 9
Business-Konflikte werden oft 409 Conflict zugeordnet.
Falle 10
Gib keine Stack Traces oder internen Exception-Details an API-Clients weiter.
42. Echte Prüfungsfrage: @ExceptionHandler
Frage:
Was macht @ExceptionHandler?
Antwort:
@ExceptionHandler markiert eine Methode, die bestimmte Exception-Typen behandelt, die von Controller-Methoden geworfen werden.
43. Echte Prüfungsfrage: Lokaler vs. globaler Handler
Frage:
Was ist der Unterschied zwischen einem @ExceptionHandler in einem Controller und einem in @ControllerAdvice?
Antwort:
Ein @ExceptionHandler in einem Controller behandelt Exceptions für diesen Controller. Ein @ExceptionHandler in @ControllerAdvice kann global über mehrere Controller hinweg gelten.
44. Echte Prüfungsfrage: @ControllerAdvice
Frage:
Wofür wird @ControllerAdvice genutzt?
Antwort:
@ControllerAdvice wird für controller-übergreifende Belange genutzt — besonders globales Exception Handling in Spring MVC.
45. Echte Prüfungsfrage: @RestControllerAdvice
Frage:
Was ist @RestControllerAdvice?
Antwort:
@RestControllerAdvice ist eine Convenience-Annotation, die @ControllerAdvice und @ResponseBody kombiniert — Rückgabewerte von Exception Handlern landen direkt im Response-Body.
46. Echte Prüfungsfrage: @ResponseStatus
Frage:
Was macht @ResponseStatus?
Antwort:
@ResponseStatus setzt einen festen HTTP-Status für eine Controller-Methode oder Exception-Klasse.
47. Echte Prüfungsfrage: ResponseStatusException
Frage:
Wofür wird ResponseStatusException genutzt?
Antwort:
ResponseStatusException wirft eine Exception mit einem bestimmten HTTP-Status und Grund — oft für einfache Fälle wie 404 Not Found.
48. Echte Prüfungsfrage: Validierungsfehler
Frage:
Welche Exception tritt üblicherweise auf, wenn @Valid @RequestBody-Validierung in Spring MVC fehlschlägt?
Antwort:
MethodArgumentNotValidException.
49. Echte Prüfungsfrage: Ungültiges JSON
Frage:
Welchen Status sollte ungültiges JSON üblicherweise zurückgeben?
Antwort:
400 Bad Request.
50. Echte Prüfungsfrage: Not Found
Frage:
Welchen Status sollte eine fehlende Ressource üblicherweise zurückgeben?
Antwort:
404 Not Found.
51. Echte Prüfungsfrage: Conflict
Frage:
Welcher Status wird üblicherweise für einen Business-Konflikt genutzt?
Antwort:
409 Conflict.
52. Echte Prüfungsfrage: 401 vs. 403
Frage:
Was ist der Unterschied zwischen 401 Unauthorized und 403 Forbidden?
Antwort:
401 Unauthorized bedeutet: Authentifizierung fehlt oder ist ungültig. 403 Forbidden bedeutet: Der Nutzer ist authentifiziert, darf die Aktion aber nicht ausführen.
53. Echte Prüfungsfrage: ProblemDetail
Frage:
Was ist ProblemDetail?
Antwort:
ProblemDetail ist eine Standardstruktur für HTTP-API-Fehlerantworten. Sie enthält Felder wie status, title, detail, type und instance — plus eigene Properties.
54. Echte Prüfungsfrage: ResponseEntityExceptionHandler
Frage:
Was ist ResponseEntityExceptionHandler?
Antwort:
ResponseEntityExceptionHandler ist eine praktische Basisklasse für viele eingebaute Spring-MVC-Exceptions und strukturierte Fehlerantworten.
55. Interview-Antwort
Frage:
Wie behandelst du Exceptions in einer Spring Boot REST API?
Gute Antwort:
Ich erstelle üblicherweise eigene Domain Exceptions wie ResourceNotFoundException und behandle sie global mit @RestControllerAdvice. In dieser Advice-Klasse nutze ich @ExceptionHandler-Methoden, um Exceptions HTTP-Statuscodes und strukturierte Fehlerantworten zuzuordnen. Not Found wird z. B. 404, Validierungsfehler 400, Business-Konflikte 409. Außerdem gibt es einen generischen Fallback-Handler für unerwartete Exceptions — interne Details werden geloggt, nicht an den Client gegeben.
56. Interview-Antwort
Frage:
Was ist der Unterschied zwischen
@ExceptionHandlerund@ControllerAdvice?
Gute Antwort:
@ExceptionHandler markiert eine Methode für einen bestimmten Exception-Typ. Im Controller gilt sie nur für diesen Controller. @ControllerAdvice lässt Exception-Handling-Methoden über mehrere Controller hinweg gelten. Für REST APIs nutze ich meist @RestControllerAdvice — das kombiniert @ControllerAdvice mit @ResponseBody.
57. Interview-Antwort
Frage:
Wie behandelst du Validierungsfehler?
Gute Antwort:
Für Request-Body-Validierung setze ich Bean-Validation-Annotations auf das Request-DTO und @Valid @RequestBody im Controller. Schlägt die Validierung fehl, wirft Spring MVC üblicherweise MethodArgumentNotValidException. Die behandle ich in einem globalen @RestControllerAdvice und liefere strukturiert 400 Bad Request mit Feldfehlern — Feldname und Validierungsmeldung.
58. Interview-Antwort
Frage:
Warum solltest du keine rohen Exception-Meldungen an Clients zurückgeben?
Gute Antwort:
Rohe Exception-Meldungen können interne Details preisgeben: Klassennamen, SQL-Queries, Tabellennamen, Dateipfade, Stack Traces oder sensible Daten. Eine produktive API loggt Fehler intern detailliert — gibt aber sichere, stabile, client-freundliche Meldungen mit passenden Status- und Error-Codes zurück.
59. Interview-Antwort
Frage:
Wann würdest du
ResponseStatusExceptionnutzen?
Gute Antwort:
Ich nutze ResponseStatusException für einfache Fälle, in denen ich schnell einen bestimmten HTTP-Status wie 404 Not Found brauche. Für größere Anwendungen bevorzuge ich eigene Exceptions plus globales @RestControllerAdvice — einheitliches Fehlerformat und stärkere Domain-Bedeutung.
60. Kleines Code-Übungsbeispiel
Erstelle eine Exception:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Object id) {
super(resource + " not found with id " + id);
}
}
Erstelle ein Error-DTO:
public record ApiErrorResponse(
Instant timestamp,
int status,
String error,
String code,
String message,
String path
) {
}
Erstelle einen globalen Handler:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse error = new ApiErrorResponse(
Instant.now(),
404,
"Not Found",
"RESOURCE_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
Fragen:
- Welche Annotation macht diesen Handler global?
- Welche Exception behandelt er?
- Welchen HTTP-Status liefert er?
- Warum
HttpServletRequestnutzen? - Warum ein Error-DTO nutzen?
Antworten:
@RestControllerAdviceResourceNotFoundException404 Not Found- Um den Request-Pfad zu lesen.
- Um eine einheitliche JSON-Fehlerantwort zurückzugeben.
61. Kleine Bug-Übung 1
Problem:
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle(Exception ex) {
return ResponseEntity.internalServerError().body(ex.toString());
}
Frage:
Was ist falsch?
Antwort:
Es gibt rohe Exception-Details an den Client weiter. Das kann interne Klassennamen, Stack Traces, SQL-Details oder sensible Informationen preisgeben. Logge die Exception intern und gib eine sichere generische Meldung zurück.
62. Kleine Bug-Übung 2
Problem:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ApiErrorResponse handle(ResourceNotFoundException ex) {
return new ApiErrorResponse(...);
}
}
Die REST API liefert nicht das erwartete JSON.
Frage:
Was könnte fehlen?
Antwort:
Einfaches @ControllerAdvice enthält nicht automatisch @ResponseBody. Für REST APIs nutze @RestControllerAdvice oder @ResponseBody auf der Handler-Methode.
63. Kleine Bug-Übung 3
Problem:
@PostMapping("/api/tasks")
public TaskDto create(@RequestBody CreateTaskRequest request) {
return taskService.create(request);
}
DTO:
public record CreateTaskRequest(
@NotBlank String title
) {
}
Die Validierung läuft nicht.
Frage:
Was fehlt?
Antwort:
@Valid fehlt.
Fix:
@PostMapping("/api/tasks")
public TaskDto create(@Valid @RequestBody CreateTaskRequest request) {
return taskService.create(request);
}
Übungsfragen
Frage 1
Warum brauchen REST APIs gutes Exception Handling?
Antwort:
REST APIs brauchen gutes Exception Handling für klare Statuscodes, einheitliche Error-Bodies, sichere Meldungen und nützliche Error-Codes — ohne interne Details preiszugeben.
Frage 2
Was macht @ExceptionHandler?
Antwort:
@ExceptionHandler markiert eine Methode, die bestimmte Exception-Typen behandelt, die von Controller-Methoden geworfen werden.
Frage 3
Was ist ein lokaler Exception Handler?
Antwort:
Ein lokaler Exception Handler ist eine @ExceptionHandler-Methode in einem Controller. Er behandelt Exceptions für diesen Controller.
Frage 4
Was macht @ControllerAdvice?
Antwort:
@ControllerAdvice liefert controller-übergreifendes Verhalten — üblicherweise globales Exception Handling.
Frage 5
Was enthält @RestControllerAdvice?
Antwort:
@RestControllerAdvice enthält @ControllerAdvice und @ResponseBody.
Frage 6
Welche Struktur ist gut für eine API-Fehlerantwort?
Antwort:
Eine gute API-Fehlerantwort kann timestamp, status, error, code, message, path und bei Validierungsfehlern field errors enthalten.
Frage 7
Warum solltest du keine Stack Traces an API-Clients geben?
Antwort:
Stack Traces können interne Klassennamen, SQL-Details, Dateipfade, Package-Namen und sensible Implementierungsdetails preisgeben.
Frage 8
Was macht @ResponseStatus?
Antwort:
@ResponseStatus setzt einen festen HTTP-Status für eine Controller-Methode oder Exception-Klasse.
Frage 9
Was ist ResponseStatusException?
Antwort:
ResponseStatusException ist eine Exception mit HTTP-Status und optionalem Grund — nützlich für einfache HTTP-Fehler.
Frage 10
Was ist ProblemDetail?
Antwort:
ProblemDetail ist eine Standardstruktur für HTTP-API-Fehlerantworten mit Feldern wie status, title, detail, type und instance.
Frage 11
Wie behandelst du Validierungsfehler von @Valid @RequestBody?
Antwort:
Behandle MethodArgumentNotValidException in einem globalen @RestControllerAdvice und liefere strukturiert 400 Bad Request mit Feldfehlern.
Frage 12
Welche Exception steht üblicherweise für fehlgeschlagene Request-Body-Validierung?
Antwort:
MethodArgumentNotValidException.
Frage 13
Welchen Status sollte ungültiges JSON zurückgeben?
Antwort:
Ungültiges JSON sollte üblicherweise 400 Bad Request zurückgeben.
Frage 14
Welchen Status sollten fehlende Ressourcen zurückgeben?
Antwort:
Fehlende Ressourcen sollten üblicherweise 404 Not Found zurückgeben.
Frage 15
Welchen Status können Business-Konflikte zurückgeben?
Antwort:
Business-Konflikte können 409 Conflict zurückgeben.
Frage 16
Was ist der Unterschied zwischen 401 und 403?
Antwort:
401 Unauthorized bedeutet: Authentifizierung fehlt oder ist ungültig. 403 Forbidden bedeutet: authentifiziert, aber nicht berechtigt.
Frage 17
Was ist ResponseEntityExceptionHandler?
Antwort:
ResponseEntityExceptionHandler ist eine praktische Basisklasse für viele eingebaute Spring-MVC-Exceptions.
Frage 18
Warum ist ein generischer Exception-Handler nützlich?
Antwort:
Ein generischer Exception Handler ist als letzter Fallback für unerwartete Fehler nützlich.
Frage 19
Was sollte ein generischer Exception Handler vermeiden?
Antwort:
Er sollte keine rohen Exception-Details, Stack Traces, SQL-Details, internen Klassennamen oder sensible Daten an den Client geben.
Frage 20
Warum ist globales Exception Handling besser als try-catch in jeder Controller-Methode?
Antwort:
Globales Exception Handling vermeidet wiederholte try-catch-Blöcke, hält Controller sauber und sorgt für ein einheitliches Fehlerantwort-Format.
Merksätze zum Mitnehmen
@ExceptionHandlerbehandelt bestimmte Exceptions.- Lokale Exception Handler gelten für einen Controller.
@ControllerAdvicegilt über Controller hinweg.@RestControllerAdviceentspricht@ControllerAdviceplus@ResponseBody.- Nutze globales Exception Handling für einheitliche REST-API-Fehler.
@ResponseStatussetzt einen festen HTTP-Status.ResponseStatusExceptionist nützlich für einfache HTTP-Fehler.ProblemDetailist eine Standard-Fehlerantwort-Struktur.MethodArgumentNotValidExceptionsteht üblicherweise für fehlgeschlagenes@Valid @RequestBody.- Ungültiges JSON liefert üblicherweise 400.
- Fehlende Ressourcen liefern üblicherweise 404.
- Business-Konflikte liefern oft 409.
- 401 bedeutet: nicht authentifiziert.
- 403 bedeutet: authentifiziert, aber nicht berechtigt.
ResponseEntityExceptionHandlerhilft bei eingebauten MVC-Exceptions.- Gib keine Stack Traces oder internen Details an Clients weiter.
- Logge detaillierte Fehler intern.
- Gib sichere, stabile, strukturierte Fehlerantworten zurück.