Przewodnik po obsłudze błędów interfejsu Spring Boot REST API
Opublikowany: 2022-03-11Poprawna obsługa błędów w interfejsach API przy jednoczesnym dostarczaniu znaczących komunikatów o błędach jest bardzo pożądaną funkcją, ponieważ może pomóc klientowi API we właściwym reagowaniu na problemy. Domyślne zachowanie zwykle zwraca ślady stosu, które są trudne do zrozumienia i ostatecznie bezużyteczne dla klienta API. Podział informacji o błędach na pola umożliwia również klientowi API przeanalizowanie ich i dostarczenie użytkownikowi lepszych komunikatów o błędach. W tym artykule omówimy, jak prawidłowo obsługiwać błędy podczas budowania interfejsu API REST za pomocą Spring Boot.
Budowanie interfejsów API REST za pomocą Springa stało się standardowym podejściem dla programistów Java w ciągu ostatnich kilku lat. Korzystanie ze Spring Boot bardzo pomaga, ponieważ usuwa wiele standardowych kodów i umożliwia automatyczną konfigurację różnych komponentów. Założymy, że znasz podstawy programowania API przy użyciu tych technologii, zanim zastosujesz opisaną tutaj wiedzę. Jeśli nadal nie masz pewności, jak opracować podstawowy interfejs API REST, powinieneś zacząć od tego artykułu o Spring MVC lub innego o tworzeniu usługi Spring REST.
Wyraźniejsze odpowiedzi na błędy
W tym artykule będziemy używać kodu źródłowego hostowanego na GitHub aplikacji, która implementuje interfejs API REST do pobierania obiektów reprezentujących ptaki. Zawiera funkcje opisane w tym artykule i kilka innych przykładów scenariuszy obsługi błędów. Oto podsumowanie punktów końcowych zaimplementowanych w tej aplikacji:
GET /birds/{birdId} | Pobiera informacje o ptaku i zgłasza wyjątek, jeśli nie zostanie znaleziony. |
GET /birds/noexception/{birdId} | To wywołanie pobiera również informacje o ptaku, z wyjątkiem tego, że nie zgłasza wyjątku w przypadku, gdy ptak nie zostanie znaleziony. | POST /birds | Tworzy ptaka. | </tr>
Moduł Spring Framework MVC zawiera kilka świetnych funkcji, które pomagają w obsłudze błędów. Ale to programista może użyć tych funkcji do obsługi wyjątków i zwracania znaczących odpowiedzi do klienta interfejsu API.
Spójrzmy na przykład domyślnej odpowiedzi Spring Boot, gdy wysyłamy HTTP POST do punktu końcowego /birds
z następującym obiektem JSON, który ma ciąg „aaa” w polu „mass”, który powinien oczekiwać liczby całkowitej:
{ "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }
Domyślna odpowiedź Spring Boot, bez właściwej obsługi błędów:
{ "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" }
Cóż… wiadomość z odpowiedzią ma kilka dobrych pól, ale jest zbyt skoncentrowana na tym, czym był wyjątek. Nawiasem mówiąc, jest to klasa DefaultErrorAttributes
z Spring Boot. Pole timestamp
jest liczbą całkowitą, która nie zawiera nawet informacji o jednostce miary, w której znajduje się znacznik czasu. Pole exception
jest interesujące tylko dla programistów Javy, a wiadomość pozostawia konsumenta API zagubionego we wszystkich szczegółach implementacji, które są dla niego nieistotne . A co by było, gdyby było więcej szczegółów, które moglibyśmy wydobyć z wyjątku, z którego pochodzi błąd? Nauczmy się więc, jak właściwie traktować te wyjątki i zapakować je w ładniejszą reprezentację JSON, aby ułatwić życie naszym klientom API.
Ponieważ będziemy używać klas daty i czasu Java 8, najpierw musimy dodać zależność Maven dla konwerterów Jackson JSR310. Zajmują się konwersją klas daty i czasu Java 8 na reprezentację JSON przy użyciu adnotacji @JsonFormat
:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
Ok, więc zdefiniujmy klasę do reprezentowania błędów API. Stworzymy klasę o nazwie ApiError
, która będzie miała wystarczającą ilość pól do przechowywania istotnych informacji o błędach występujących podczas wywołań REST.
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(); } }
Właściwość
status
przechowuje status wywołania operacji. Będzie to wszystko od 4xx do sygnalizowania błędów klienta lub 5xx do oznaczania błędów serwera. Typowym scenariuszem jest kod http 400, który oznacza BAD_REQUEST, gdy klient na przykład wysyła niewłaściwie sformatowane pole, takie jak nieprawidłowy adres e-mail.Właściwość
timestamp
przechowuje instancję daty i godziny, w której wystąpił błąd.Właściwość
message
zawiera przyjazny dla użytkownika komunikat o błędzie.Właściwość
debugMessage
zawiera komunikat systemowy opisujący bardziej szczegółowo błąd.Właściwość
subErrors
zawiera tablicę błędów podrzędnych, które wystąpiły. Służy do reprezentowania wielu błędów w jednym wywołaniu. Przykładem mogą być błędy walidacji, w których wiele pól nie przeszło walidacji. KlasaApiSubError
służy do ich enkapsulacji.
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; } }
Zatem ApiValidationError
jest klasą, która rozszerza ApiSubError
i wyraża problemy z walidacją napotkane podczas wywołania REST.
Poniżej zobaczysz kilka przykładów odpowiedzi JSON, które są generowane po zaimplementowaniu opisanych tutaj ulepszeń, aby zorientować się, co będziemy mieli pod koniec tego artykułu.
Oto przykład JSON zwracanego, gdy encja nie zostanie znaleziona podczas wywoływania punktu końcowego GET /birds/2
:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }
Oto kolejny przykład JSON zwrócony podczas wydawania wywołania POST /birds
z nieprawidłową wartością masy ptaka:
{ "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" } ] } }
Obsługa błędów podczas rozruchu sprężynowego
Przyjrzyjmy się niektórym adnotacjom Spring, które będą używane do obsługi wyjątków.

RestController
to podstawowa adnotacja dla klas obsługujących operacje REST.
ExceptionHandler
to adnotacja Spring, która zapewnia mechanizm obsługi wyjątków, które są zgłaszane podczas wykonywania programów obsługi (operacje kontrolera). Ta adnotacja, jeśli jest używana w metodach klas kontrolerów, będzie służyć jako punkt wejścia do obsługi wyjątków zgłoszonych tylko w tym kontrolerze. Ogólnie rzecz biorąc, najczęstszym sposobem jest użycie @ExceptionHandler
na metodach klas @ControllerAdvice
, aby obsługa wyjątków była stosowana globalnie lub do podzbioru kontrolerów.
ControllerAdvice
to adnotacja wprowadzona w Spring 3.2 i jak sama nazwa wskazuje, to „Porada” dla wielu kontrolerów. Służy do włączenia jednego ExceptionHandler
do wielu kontrolerów. W ten sposób możemy w jednym miejscu zdefiniować sposób traktowania takiego wyjątku i ten handler zostanie wywołany, gdy wyjątek zostanie zgłoszony z klas, które są objęte tym ControllerAdvice
. Podzbiór kontrolerów, których to dotyczy, można zdefiniować za pomocą następujących selektorów w @ControllerAdvice
: annotations()
, basePackageClasses()
i basePackages()
. Jeśli nie podano żadnych selektorów, ControllerAdvice
jest stosowany globalnie do wszystkich kontrolerów.
Używając @ExceptionHandler
i @ControllerAdvice
, będziemy mogli zdefiniować centralny punkt obsługi wyjątków i pakowania ich w obiekt ApiError
z lepszą organizacją niż domyślny mechanizm obsługi błędów Spring Boot.
Wyjątki dotyczące obsługi
Następnym krokiem jest stworzenie klasy, która będzie obsługiwać wyjątki. Dla uproszczenia nazywamy go RestExceptionHandler
i musi on rozciągać się od ResponseEntityExceptionHandler
w Spring Boot. Będziemy rozszerzać ResponseEntityExceptionHandler
, ponieważ już zapewnia podstawową obsługę wyjątków Spring MVC, więc dodamy obsługę nowych wyjątków, jednocześnie ulepszając istniejące.
Zastępowanie wyjątków obsługiwanych w ResponseEntityExceptionHandler
Jeśli zajrzysz do kodu źródłowego ResponseEntityExceptionHandler
, zobaczysz wiele metod o nazwie handle******()
, takich jak handleHttpMessageNotReadable()
lub handleHttpMessageNotWritable()
. Zobaczmy najpierw, jak możemy rozszerzyć handleHttpMessageNotReadable()
o obsługę wyjątków HttpMessageNotReadableException
. Musimy tylko nadpisać metodę handleHttpMessageNotReadable()
w naszej klasie 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 }
Zadeklarowaliśmy, że w przypadku wyrzucenia HttpMessageNotReadableException
, komunikat o błędzie będzie miał postać „Malformed JSON request”, a błąd zostanie zahermetyzowany wewnątrz obiektu ApiError
. Poniżej możemy zobaczyć odpowiedź na wywołanie REST z zastąpioną nową metodą:
{ "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]" } }
Obsługa wyjątków niestandardowych
Zobaczymy teraz, jak utworzyć metodę, która obsługuje wyjątek, który nie jest jeszcze zadeklarowany w ResponseEntityExceptionHandler
w Spring Boot.
Typowym scenariuszem dla aplikacji Spring, która obsługuje wywołania bazy danych, jest wywołanie rekordu na podstawie jego identyfikatora przy użyciu klasy repozytorium. Ale jeśli zajrzymy do metody CrudRepository.findOne()
, zobaczymy, że zwraca ona wartość null
, jeśli obiekt nie zostanie znaleziony. Oznacza to, że jeśli nasza usługa po prostu wywoła tę metodę i wróci bezpośrednio do kontrolera, otrzymamy kod HTTP 200 (OK), nawet jeśli zasób nie zostanie znaleziony. W rzeczywistości właściwym podejściem jest zwrócenie kodu HTTP 404 (NOT FOUND) zgodnie ze specyfikacją HTTP/1.1.
Aby obsłużyć ten przypadek, utworzymy niestandardowy wyjątek o nazwie EntityNotFoundException
. Ten wyjątek jest niestandardowym wyjątkiem i różni się od javax.persistence.EntityNotFoundException
, ponieważ zapewnia kilka konstruktorów, które ułatwiają tworzenie obiektów i można wybrać inny sposób obsługi wyjątku javax.persistence
.
To powiedziawszy, utwórzmy ExceptionHandler
dla tego nowo utworzonego EntityNotFoundException
w naszej klasie RestExceptionHandler
. Aby to zrobić, utwórz metodę o nazwie handleEntityNotFound()
i dodaj do niej adnotację @ExceptionHandler
, przekazując do niej obiekt klasy EntityNotFoundException.class
. To sygnalizuje Springowi, że za każdym razem, gdy zostanie EntityNotFoundException
, Spring powinien wywołać tę metodę, aby go obsłużyć. Podczas dodawania adnotacji do metody za pomocą @ExceptionHandler
, zaakceptuje ona szeroki zakres automatycznie wstrzykiwanych parametrów, takich jak WebRequest
, Locale
i inne, jak opisano tutaj. Podamy tylko sam wyjątek EntityNotFoundException
jako parametr dla tej metody handleEntityNotFound
.
@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); } }
Świetnie! W handleEntityNotFound()
ustawiamy kod statusu HTTP na NOT_FOUND
i używamy nowego komunikatu o wyjątku. Oto jak teraz wygląda odpowiedź dla punktu końcowego GET /birds/2
:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }
Wniosek
Ważne jest, aby przejąć kontrolę nad obsługą wyjątków, abyśmy mogli odpowiednio zmapować te wyjątki do obiektu ApiError
i dostarczyć ważne informacje, które pozwolą klientom API wiedzieć, co się stało. Następnym krokiem od tego miejsca byłoby utworzenie większej liczby metod obsługi (tych z @ExceptionHandler) dla wyjątków, które są zgłaszane w kodzie aplikacji. Istnieje więcej przykładów innych typowych wyjątków, takich jak MethodArgumentTypeMismatchException
, ConstraintViolationException
i inne w kodzie GitHub.
Oto kilka dodatkowych zasobów, które pomogły w tworzeniu tego artykułu:
Baeldung - Obsługa błędów dla REST ze sprężyną
Spring Blog - Obsługa wyjątków w Spring MVC