Leitfaden zur Fehlerbehandlung der Spring Boot-REST-API
Veröffentlicht: 2022-03-11Die korrekte Behandlung von Fehlern in APIs bei gleichzeitiger Bereitstellung aussagekräftiger Fehlermeldungen ist eine sehr wünschenswerte Funktion, da sie dem API-Client helfen kann, richtig auf Probleme zu reagieren. Das Standardverhalten neigt dazu, Stack-Traces zurückzugeben, die schwer zu verstehen und letztendlich für den API-Client nutzlos sind. Die Partitionierung der Fehlerinformationen in Felder ermöglicht es dem API-Client auch, sie zu analysieren und dem Benutzer bessere Fehlermeldungen bereitzustellen. In diesem Artikel behandeln wir die richtige Fehlerbehandlung beim Erstellen einer REST-API mit Spring Boot.
Das Erstellen von REST-APIs mit Spring wurde in den letzten Jahren zum Standardansatz für Java-Entwickler. Die Verwendung von Spring Boot hilft erheblich, da es eine Menge Boilerplate-Code entfernt und die automatische Konfiguration verschiedener Komponenten ermöglicht. Wir gehen davon aus, dass Sie mit den Grundlagen der API-Entwicklung mit diesen Technologien vertraut sind, bevor Sie das hier beschriebene Wissen anwenden. Wenn Sie sich immer noch nicht sicher sind, wie Sie eine grundlegende REST-API entwickeln, sollten Sie mit diesem Artikel über Spring MVC oder einem anderen über das Erstellen eines Spring-REST-Dienstes beginnen.
Fehlerantworten klarer machen
In diesem Artikel verwenden wir den auf GitHub gehosteten Quellcode einer Anwendung, die eine REST-API zum Abrufen von Objekten implementiert, die Vögel darstellen. Es verfügt über die in diesem Artikel beschriebenen Funktionen und einige weitere Beispiele für Fehlerbehandlungsszenarien. Hier ist eine Zusammenfassung der in dieser Anwendung implementierten Endpunkte:
GET /birds/{birdId} | Ruft Informationen über einen Vogel ab und löst eine Ausnahme aus, wenn er nicht gefunden wird. |
GET /birds/noexception/{birdId} | Dieser Aufruf erhält auch Informationen über einen Vogel, außer dass er keine Ausnahme auslöst, falls der Vogel nicht gefunden wird. | POST /birds | Erstellt einen Vogel. | </tr>
Das MVC-Modul des Spring-Frameworks enthält einige großartige Funktionen, die bei der Fehlerbehandlung helfen. Es bleibt jedoch dem Entwickler überlassen, diese Funktionen zu verwenden, um die Ausnahmen zu behandeln und aussagekräftige Antworten an den API-Client zurückzugeben.
Sehen wir uns ein Beispiel für die standardmäßige Spring Boot-Antwort an, wenn wir einen HTTP-POST an den /birds
-Endpunkt mit dem folgenden JSON-Objekt ausgeben, das die Zeichenfolge „aaa“ im Feld „mass“ enthält, das eine Ganzzahl erwarten sollte:
{ "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }
Die Spring Boot-Standardantwort ohne ordnungsgemäße Fehlerbehandlung:
{ "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }
Nun… die Antwortnachricht hat einige gute Felder, aber sie konzentriert sich zu sehr darauf, was die Ausnahme war. Das ist übrigens die Klasse DefaultErrorAttributes
von Spring Boot. Das timestamp
ist eine ganze Zahl, die nicht einmal Informationen darüber enthält, in welcher Maßeinheit der Zeitstempel vorliegt. Das exception
ist nur für Java-Entwickler interessant und die Nachricht lässt den API-Verbraucher in allen für ihn irrelevanten Implementierungsdetails verloren . Und was wäre, wenn wir mehr Details aus der Ausnahme extrahieren könnten, aus der der Fehler stammt? Lassen Sie uns also lernen, wie man diese Ausnahmen richtig behandelt und sie in eine schönere JSON-Darstellung verpackt, um unseren API-Clients das Leben zu erleichtern.
Da wir Datums- und Zeitklassen von Java 8 verwenden werden, müssen wir zuerst eine Maven-Abhängigkeit für die Jackson JSR310-Konverter hinzufügen. Sie kümmern sich um die Konvertierung von Java 8-Datums- und Zeitklassen in die JSON-Darstellung mithilfe der Annotation @JsonFormat
:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
Ok, also definieren wir eine Klasse zur Darstellung von API-Fehlern. Wir erstellen eine Klasse namens ApiError
, die über genügend Felder verfügt, um relevante Informationen zu Fehlern zu enthalten, die während REST-Aufrufen auftreten.
class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
Die
status
enthält den Vorgangsaufrufstatus. Es kann alles sein, von 4xx, um Clientfehler zu signalisieren, oder 5xx, um Serverfehler zu bedeuten. Ein häufiges Szenario ist ein http-Code 400, der eine BAD_REQUEST bedeutet, wenn der Client beispielsweise ein falsch formatiertes Feld wie eine ungültige E-Mail-Adresse sendet.Die
timestamp
-Eigenschaft enthält die Datums-/Uhrzeitinstanz des Auftretens des Fehlers.Die
message
enthält eine benutzerfreundliche Nachricht über den Fehler.Die Eigenschaft
debugMessage
enthält eine Systemmeldung, die den Fehler detaillierter beschreibt.Die Eigenschaft
subErrors
enthält ein Array von aufgetretenen Teilfehlern. Dies wird verwendet, um mehrere Fehler in einem einzigen Aufruf darzustellen. Ein Beispiel wären Validierungsfehler, bei denen mehrere Felder die Validierung nicht bestanden haben. DieApiSubError
-Klasse wird verwendet, um diese zu kapseln.
abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }
ApiValidationError
ist also eine Klasse, die ApiSubError
erweitert und Validierungsprobleme ausdrückt, die während des REST-Aufrufs aufgetreten sind.
Unten sehen Sie einige Beispiele für JSON-Antworten, die generiert werden, nachdem wir die hier beschriebenen Verbesserungen implementiert haben, nur um eine Vorstellung davon zu bekommen, was wir am Ende dieses Artikels haben werden.
Hier ist ein Beispiel für JSON, das zurückgegeben wird, wenn eine Entität beim Aufrufen des Endpunkts GET /birds/2
nicht gefunden wird:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }
Hier ist ein weiteres Beispiel für JSON, das zurückgegeben wird, wenn ein POST /birds
-Aufruf mit einem ungültigen Wert für die Masse des Vogels ausgegeben wird:
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }
Spring Boot-Fehlerbehandlung
Sehen wir uns einige der Spring-Anmerkungen an, die zur Behandlung von Ausnahmen verwendet werden.

RestController
ist die Basisannotation für Klassen, die REST-Vorgänge verarbeiten.
ExceptionHandler
ist eine Spring-Annotation, die einen Mechanismus zum Behandeln von Ausnahmen bereitstellt, die während der Ausführung von Handlern (Controller-Operationen) ausgelöst werden. Diese Annotation dient, wenn sie für Methoden von Controller-Klassen verwendet wird, als Einstiegspunkt für die Behandlung von Ausnahmen, die nur innerhalb dieses Controllers ausgelöst werden. Insgesamt besteht die häufigste Methode darin, @ExceptionHandler
für Methoden von @ControllerAdvice
Klassen zu verwenden, sodass die Ausnahmebehandlung global oder auf eine Teilmenge von Controllern angewendet wird.
ControllerAdvice
ist eine Anmerkung, die in Spring 3.2 eingeführt wurde, und ist, wie der Name schon sagt, „Advice“ für mehrere Controller. Es wird verwendet, um zu ermöglichen, dass ein einzelner ExceptionHandler
auf mehrere Controller angewendet wird. Auf diese Weise können wir an nur einer Stelle definieren, wie eine solche Ausnahme behandelt werden soll, und dieser Handler wird aufgerufen, wenn die Ausnahme von Klassen ausgelöst wird, die von dieser ControllerAdvice
abgedeckt werden. Die Teilmenge der betroffenen Controller kann mithilfe der folgenden Selektoren für @ControllerAdvice
definiert werden: annotations()
, basePackageClasses()
und basePackages()
. Wenn keine Selektoren bereitgestellt werden, wird die ControllerAdvice
global auf alle Controller angewendet.
Durch die Verwendung von @ExceptionHandler
und @ControllerAdvice
können wir also einen zentralen Punkt für die Behandlung von Ausnahmen definieren und sie in einem ApiError
-Objekt mit besserer Organisation als der standardmäßige Spring Boot-Fehlerbehandlungsmechanismus verpacken.
Umgang mit Ausnahmen
Der nächste Schritt besteht darin, die Klasse zu erstellen, die die Ausnahmen behandelt. Der Einfachheit halber nennen wir es RestExceptionHandler
und es muss von Spring Boot's ResponseEntityExceptionHandler
erweitert werden. Wir werden ResponseEntityExceptionHandler
erweitern, da es bereits eine grundlegende Behandlung von Spring MVC-Ausnahmen bietet, also werden wir Handler für neue Ausnahmen hinzufügen und gleichzeitig die vorhandenen verbessern.
Überschreiben von Ausnahmen, die in ResponseEntityExceptionHandler behandelt werden
Wenn Sie sich den Quellcode von ResponseEntityExceptionHandler
ansehen, werden Sie viele Methoden namens handle******()
sehen, wie handleHttpMessageNotReadable()
oder handleHttpMessageNotWritable()
. Sehen wir uns zunächst an, wie wir handleHttpMessageNotReadable()
erweitern können, um HttpMessageNotReadableException
Ausnahmen zu behandeln. Wir müssen nur die Methode handleHttpMessageNotReadable()
in unserer Klasse RestExceptionHandler
:
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }
Wir haben erklärt, dass im Falle einer HttpMessageNotReadableException
die Fehlermeldung „Malformed JSON request“ lautet und der Fehler im ApiError
Objekt gekapselt wird. Unten sehen wir die Antwort eines REST-Aufrufs mit dieser neuen überschriebenen Methode:
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }
Umgang mit benutzerdefinierten Ausnahmen
Jetzt sehen wir uns an, wie man eine Methode erstellt, die eine Ausnahme behandelt, die noch nicht im ResponseEntityExceptionHandler
von Spring Boot deklariert ist.
Ein häufiges Szenario für eine Spring-Anwendung, die Datenbankaufrufe verarbeitet, ist ein Aufruf, um einen Datensatz anhand seiner ID mithilfe einer Repository-Klasse zu finden. Aber wenn wir uns die Methode CrudRepository.findOne()
, sehen wir, dass sie null
zurückgibt, wenn ein Objekt nicht gefunden wird. Das heißt, wenn unser Dienst diese Methode einfach aufruft und direkt zum Controller zurückkehrt, erhalten wir einen HTTP-Code 200 (OK), auch wenn die Ressource nicht gefunden wird. Tatsächlich besteht der richtige Ansatz darin, einen HTTP-Code 404 (NOT FOUND) zurückzugeben, wie in der HTTP/1.1-Spezifikation angegeben.
Um diesen Fall zu behandeln, erstellen wir eine benutzerdefinierte Ausnahme namens EntityNotFoundException
. Dies ist eine benutzerdefinierte Ausnahme und unterscheidet sich von javax.persistence.EntityNotFoundException
, da sie einige Konstruktoren bereitstellt, die die Objekterstellung erleichtern, und man kann die javax.persistence
Ausnahme anders behandeln.
Lassen Sie uns dennoch einen ExceptionHandler
für diese neu erstellte EntityNotFoundException
in unserer RestExceptionHandler
-Klasse erstellen. Erstellen Sie dazu eine Methode namens handleEntityNotFound()
und kommentieren Sie sie mit @ExceptionHandler
, indem Sie ihr das Klassenobjekt EntityNotFoundException.class
. Dies signalisiert Spring, dass jedes Mal, EntityNotFoundException
ausgelöst wird, Spring diese Methode aufrufen sollte, um es zu behandeln. Wenn eine Methode mit @ExceptionHandler
wird, akzeptiert sie eine Vielzahl von automatisch eingefügten Parametern wie WebRequest
, Locale
und andere, wie hier beschrieben. Wir stellen nur die Ausnahme EntityNotFoundException
selbst als Parameter für diese handleEntityNotFound
Methode bereit.
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }
Toll! In der Methode handleEntityNotFound()
setzen wir den HTTP-Statuscode auf NOT_FOUND
und verwenden die neue Ausnahmemeldung. So sieht die Antwort für den Endpunkt GET /birds/2
jetzt aus:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }
Fazit
Es ist wichtig, die Kontrolle über die Ausnahmebehandlung zu erlangen, damit wir diese Ausnahmen ordnungsgemäß dem ApiError
Objekt zuordnen und wichtige Informationen bereitstellen können, die es API-Clients ermöglichen, zu wissen, was passiert ist. Der nächste Schritt von hier aus wäre, weitere Handlermethoden (die mit @ExceptionHandler) für Ausnahmen zu erstellen, die im Anwendungscode ausgelöst werden. Es gibt weitere Beispiele für einige andere häufige Ausnahmen wie MethodArgumentTypeMismatchException
, ConstraintViolationException
und andere im GitHub-Code.
Hier sind einige zusätzliche Ressourcen, die bei der Erstellung dieses Artikels geholfen haben:
Baeldung - Fehlerbehandlung für REST mit Spring
Spring Blog - Ausnahmebehandlung in Spring MVC