Ghid pentru gestionarea erorilor API REST Spring Boot
Publicat: 2022-03-11Gestionarea corectă a erorilor în API-uri, oferind în același timp mesaje de eroare semnificative, este o caracteristică foarte de dorit, deoarece poate ajuta clientul API să răspundă corect la probleme. Comportamentul implicit tinde să returneze urme de stivă greu de înțeles și, în cele din urmă, inutile pentru clientul API. Partiționarea informațiilor de eroare în câmpuri permite, de asemenea, clientului API să le analizeze și să furnizeze mesaje de eroare mai bune utilizatorului. În acest articol, vom trata cum să gestionăm corect erorile atunci când construim un API REST cu Spring Boot.
Construirea de API-uri REST cu Spring a devenit abordarea standard pentru dezvoltatorii Java în ultimii doi ani. Utilizarea Spring Boot ajută în mod substanțial, deoarece elimină o mulțime de coduri standard și permite configurarea automată a diferitelor componente. Vom presupune că sunteți familiarizat cu elementele de bază ale dezvoltării API cu acele tehnologii înainte de a aplica cunoștințele descrise aici. Dacă încă nu sunteți sigur despre cum să dezvoltați un API REST de bază, atunci ar trebui să începeți cu acest articol despre Spring MVC sau altul despre construirea unui serviciu Spring REST.
Răspunsurile de eroare sunt mai clare
Pe parcursul acestui articol, vom folosi codul sursă găzduit pe GitHub al unei aplicații care implementează un API REST pentru recuperarea obiectelor care reprezintă păsări. Are caracteristicile descrise în acest articol și alte câteva exemple de scenarii de tratare a erorilor. Iată un rezumat al punctelor finale implementate în acea aplicație:
GET /birds/{birdId} | Obține informații despre o pasăre și aruncă o excepție dacă nu este găsită. |
GET /birds/noexception/{birdId} | Acest apel primește și informații despre o pasăre, cu excepția faptului că nu face o excepție în cazul în care pasărea nu este găsită. | POST /birds | Creează o pasăre. | </tr>
Modulul Spring framework MVC vine cu câteva caracteristici excelente pentru a ajuta la tratarea erorilor. Dar este lăsat la latitudinea dezvoltatorului să folosească aceste caracteristici pentru a trata excepțiile și a returna răspunsuri semnificative clientului API.
Să ne uităm la un exemplu de răspuns implicit Spring Boot atunci când lansăm un HTTP POST către punctul final /birds
cu următorul obiect JSON, care are șirul „aaa” în câmpul „mass”, care ar trebui să aștepte un număr întreg:
{ "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }
Răspunsul implicit Spring Boot, fără o gestionare adecvată a erorilor:
{ "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" }
Ei bine... mesajul de răspuns are câteva câmpuri bune, dar se concentrează prea mult pe ceea ce a fost excepția. Apropo, aceasta este clasa DefaultErrorAttributes
din Spring Boot. Câmpul de timestamp
este un număr întreg care nici măcar nu conține informații despre unitatea de măsură în care se află marca temporală. Câmpul de exception
este interesant doar pentru dezvoltatorii Java și mesajul îl lasă pe consumatorul API pierdut în toate detaliile de implementare care sunt irelevante pentru ei. . Și dacă ar fi mai multe detalii pe care le-am putea extrage din excepția din care provine eroarea? Așa că haideți să învățăm cum să tratăm aceste excepții în mod corespunzător și să le includem într-o reprezentare JSON mai plăcută pentru a ușura viața clienților noștri API.
Deoarece vom folosi clasele de dată și oră Java 8, mai întâi trebuie să adăugăm o dependență Maven pentru convertoarele Jackson JSR310. Ei se ocupă de conversia claselor de dată și oră Java 8 în reprezentare JSON folosind adnotarea @JsonFormat
:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
Ok, deci haideți să definim o clasă pentru reprezentarea erorilor API. Vom crea o clasă numită ApiError
care are suficiente câmpuri pentru a păstra informații relevante despre erorile care apar în timpul apelurilor 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(); } }
Proprietatea
status
deține starea apelului operațional. Va fi orice, de la 4xx pentru a semnala erori de client sau 5xx pentru a însemna erori de server. Un scenariu comun este un cod http 400 care înseamnă un BAD_REQUEST, atunci când clientul, de exemplu, trimite un câmp formatat necorespunzător, cum ar fi o adresă de e-mail nevalidă.Proprietatea
timestamp
deține instanța dată-oră a când s-a produs eroarea.Proprietatea
message
conține un mesaj ușor de utilizat despre eroare.Proprietatea
debugMessage
deține un mesaj de sistem care descrie eroarea mai detaliat.Proprietatea
subErrors
deține o serie de sub-erori care au avut loc. Acesta este utilizat pentru reprezentarea mai multor erori într-un singur apel. Un exemplu ar fi erorile de validare în care mai multe câmpuri au eșuat validarea. ClasaApiSubError
este folosită pentru încapsularea acestora.
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; } }
Deci, ApiValidationError
este o clasă care extinde ApiSubError
și exprimă problemele de validare întâlnite în timpul apelului REST.
Mai jos, veți vedea câteva exemple de răspunsuri JSON care sunt generate după ce am implementat îmbunătățirile descrise aici, doar pentru a vă face o idee despre ceea ce vom avea până la sfârșitul acestui articol.
Iată un exemplu de JSON returnat atunci când o entitate nu este găsită în timpul apelării punctului final GET /birds/2
:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }
Iată un alt exemplu de JSON returnat la emiterea unui apel POST /birds
cu o valoare nevalidă pentru masa păsării:
{ "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" } ] } }
Gestionarea erorilor Spring Boot
Să explorăm câteva dintre adnotările de primăvară care vor fi folosite pentru a gestiona excepțiile.

RestController
este adnotarea de bază pentru clasele care se ocupă de operațiuni REST.
ExceptionHandler
este o adnotare Spring care furnizează un mecanism de tratare a excepțiilor care sunt aruncate în timpul execuției handlerelor (operații Controller). Această adnotare, dacă este utilizată pe metodele claselor de controler, va servi drept punct de intrare pentru gestionarea excepțiilor aruncate numai în acest controler. În total, cea mai comună modalitate este de a folosi @ExceptionHandler
pe metodele claselor @ControllerAdvice
, astfel încât gestionarea excepțiilor să fie aplicată global sau unui subset de controlere.
ControllerAdvice
este o adnotare introdusă în Spring 3.2 și, după cum sugerează și numele, este „Advice” pentru mai multe controlere. Este folosit pentru a permite aplicarea unui singur ExceptionHandler
la mai multe controlere. În acest fel, putem defini într-un singur loc cum să tratăm o astfel de excepție și acest handler va fi apelat atunci când excepția este aruncată din clasele care sunt acoperite de acest ControllerAdvice
. Subsetul de controlere afectate poate fi definit folosind următorii selectori de pe @ControllerAdvice
: annotations()
, basePackageClasses()
și basePackages()
. Dacă nu sunt furnizați selectori, atunci ControllerAdvice
se aplică la nivel global tuturor controlerelor.
Deci, utilizând @ExceptionHandler
și @ControllerAdvice
, vom putea defini un punct central pentru tratarea excepțiilor și pentru a le împacheta într-un obiect ApiError
cu o organizare mai bună decât mecanismul implicit de gestionare a erorilor Spring Boot.
Gestionarea excepțiilor
Următorul pas este să creați clasa care va gestiona excepțiile. Pentru simplitate, îl numim RestExceptionHandler
și trebuie să se extindă din ResponseEntityExceptionHandler
de la Spring Boot. Vom extinde ResponseEntityExceptionHandler
, deoarece oferă deja o gestionare de bază a excepțiilor Spring MVC, așa că vom adăuga handlere pentru noile excepții, în timp ce le vom îmbunătăți pe cele existente.
Suprascrierea excepțiilor gestionate în ResponseEntityExceptionHandler
Dacă aruncați o privire în codul sursă al ResponseEntityExceptionHandler
, veți vedea o mulțime de metode numite handle******()
precum handleHttpMessageNotReadable()
sau handleHttpMessageNotWritable()
. Să vedem mai întâi cum putem extinde handleHttpMessageNotReadable()
pentru a gestiona excepțiile HttpMessageNotReadableException
. Trebuie doar să suprascriem metoda handleHttpMessageNotReadable()
din clasa noastră 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 }
Am declarat că în cazul în care se aruncă o HttpMessageNotReadableException
, mesajul de eroare va fi „Solicitare JSON malformed” și eroarea va fi încapsulată în obiectul ApiError
. Mai jos putem vedea răspunsul unui apel REST cu această nouă metodă înlocuită:
{ "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]" } }
Gestionarea excepțiilor personalizate
Acum vom vedea cum să creăm o metodă care gestionează o excepție care nu este încă declarată în ResponseEntityExceptionHandler
al lui Spring Boot.
Un scenariu obișnuit pentru o aplicație Spring care gestionează apelurile la baze de date este să aibă un apel pentru a găsi o înregistrare după ID-ul său folosind o clasă de depozit. Dar dacă ne uităm la metoda CrudRepository.findOne()
, vom vedea că returnează null
dacă un obiect nu este găsit. Asta înseamnă că, dacă serviciul nostru doar apelează această metodă și revine direct la controler, vom obține un cod HTTP 200 (OK) chiar dacă resursa nu este găsită. De fapt, abordarea adecvată este să returnați un cod HTTP 404 (NEGĂSIT) așa cum este specificat în specificația HTTP/1.1.
Pentru a gestiona acest caz, vom crea o excepție personalizată numită EntityNotFoundException
. Aceasta este o excepție creată personalizat și diferită de javax.persistence.EntityNotFoundException
, deoarece oferă niște constructori care ușurează crearea obiectului și se poate alege să gestioneze excepția javax.persistence
în mod diferit.
Acestea fiind spuse, să creăm un ExceptionHandler
pentru această EntityNotFoundException
nou creată în clasa noastră RestExceptionHandler
. Pentru a face asta, creați o metodă numită handleEntityNotFound()
și adnotați-o cu @ExceptionHandler
, trecându-i obiectul de clasă EntityNotFoundException.class
. Acest lucru semnalează Spring că de fiecare dată când EntityNotFoundException
este aruncată, Spring ar trebui să apeleze această metodă pentru a o gestiona. Când se adnotă o metodă cu @ExceptionHandler
, aceasta va accepta o gamă largă de parametri auto-injectați, cum ar fi WebRequest
, Locale
și alții, așa cum este descris aici. Vom furniza doar excepția EntityNotFoundException
ca parametru pentru această metodă 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); } }
Grozav! În metoda handleEntityNotFound()
, setăm codul de stare HTTP la NOT_FOUND
și folosim noul mesaj de excepție. Iată cum arată răspunsul pentru punctul final GET /birds/2
acum:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }
Concluzie
Este important să obținem controlul asupra gestionării excepțiilor, astfel încât să putem mapa în mod corespunzător aceste excepții la obiectul ApiError
și să oferim informații importante care le permit clienților API să știe ce sa întâmplat. Următorul pas de aici ar fi să creați mai multe metode de gestionare (cele cu @ExceptionHandler) pentru excepțiile care sunt aruncate în codul aplicației. Există mai multe exemple pentru alte excepții comune, cum ar fi MethodArgumentTypeMismatchException
, ConstraintViolationException
și altele în codul GitHub.
Iată câteva resurse suplimentare care au ajutat la alcătuirea acestui articol:
Baeldung - Gestionarea erorilor pentru REST cu Spring
Spring Blog - Gestionarea excepțiilor în Spring MVC