Guida alla gestione degli errori dell'API REST Spring Boot

Pubblicato: 2022-03-11

La corretta gestione degli errori nelle API fornendo al contempo messaggi di errore significativi è una funzionalità molto desiderabile, in quanto può aiutare il client API a rispondere correttamente ai problemi. Il comportamento predefinito tende a restituire tracce dello stack difficili da comprendere e in definitiva inutili per il client API. Il partizionamento delle informazioni sull'errore in campi consente inoltre al client API di analizzarle e fornire messaggi di errore migliori all'utente. In questo articolo, tratteremo come eseguire una corretta gestione degli errori durante la creazione di un'API REST con Spring Boot.

Persona confusa per un messaggio di errore criptico e lungo

La creazione di API REST con Spring è diventata l'approccio standard per gli sviluppatori Java negli ultimi due anni. L'uso di Spring Boot aiuta sostanzialmente, poiché rimuove molto codice standard e consente la configurazione automatica di vari componenti. Daremo per scontato che tu abbia familiarità con le basi dello sviluppo di API con tali tecnologie prima di applicare le conoscenze qui descritte. Se non sei ancora sicuro di come sviluppare un'API REST di base, dovresti iniziare con questo articolo su Spring MVC o un altro sulla creazione di un servizio REST di Spring.

Rendere più chiare le risposte agli errori

In questo articolo useremo il codice sorgente ospitato su GitHub di un'applicazione che implementa un'API REST per recuperare oggetti che rappresentano uccelli. Ha le funzionalità descritte in questo articolo e alcuni altri esempi di scenari di gestione degli errori. Ecco un riepilogo degli endpoint implementati in quell'applicazione:

</tr>
GET /birds/{birdId} Ottiene informazioni su un uccello e genera un'eccezione se non viene trovata.
GET /birds/noexception/{birdId} Questa chiamata ottiene anche informazioni su un uccello, tranne per il fatto che non genera un'eccezione nel caso in cui l'uccello non venga trovato.
POST /birds Crea un uccello.

Il modulo MVC del framework Spring viene fornito con alcune fantastiche funzionalità per aiutare con la gestione degli errori. Ma spetta allo sviluppatore utilizzare queste funzionalità per trattare le eccezioni e restituire risposte significative al client API.

Diamo un'occhiata a un esempio della risposta Spring Boot predefinita quando inviamo un HTTP POST all'endpoint /birds con il seguente oggetto JSON, che ha la stringa "aaa" sul campo "mass", che dovrebbe aspettarsi un numero intero:

 { "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }

La risposta predefinita di Spring Boot, senza una corretta gestione degli errori:

 { "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" }

Bene... il messaggio di risposta ha dei buoni campi, ma è troppo focalizzato su quale fosse l'eccezione. A proposito, questa è la classe DefaultErrorAttributes di Spring Boot. Il campo timestamp è un numero intero che non contiene nemmeno informazioni sull'unità di misura in cui si trova il timestamp. Il campo exception è interessante solo per gli sviluppatori Java e il messaggio lascia il consumatore dell'API perso in tutti i dettagli di implementazione che sono irrilevanti per loro . E se ci fossero più dettagli che potremmo estrarre dall'eccezione da cui ha avuto origine l'errore? Quindi impariamo come trattare correttamente queste eccezioni e avvolgerle in una rappresentazione JSON più gradevole per semplificare la vita ai nostri client API.

Poiché utilizzeremo le classi di data e ora Java 8, dobbiamo prima aggiungere una dipendenza Maven per i convertitori Jackson JSR310. Si occupano della conversione delle classi di data e ora Java 8 nella rappresentazione JSON utilizzando l'annotazione @JsonFormat :

 <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>

Ok, quindi definiamo una classe per rappresentare gli errori dell'API. Creeremo una classe chiamata ApiError che ha campi sufficienti per contenere informazioni rilevanti sugli errori che si verificano durante le chiamate 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(); } }
  • La proprietà status mantiene lo stato della chiamata dell'operazione. Sarà qualsiasi cosa, da 4xx per segnalare errori del client o 5xx per indicare errori del server. Uno scenario comune è un codice http 400 che significa BAD_REQUEST, quando il client, ad esempio, invia un campo formattato in modo errato, come un indirizzo email non valido.

  • La proprietà timestamp contiene l'istanza di data e ora di quando si è verificato l'errore.

  • La proprietà message contiene un messaggio intuitivo sull'errore.

  • La proprietà debugMessage contiene un messaggio di sistema che descrive l'errore in modo più dettagliato.

  • La proprietà subErrors contiene una matrice di errori secondari che si sono verificati. Viene utilizzato per rappresentare più errori in una singola chiamata. Un esempio potrebbero essere gli errori di convalida in cui più campi non hanno superato la convalida. La classe ApiSubError viene utilizzata per incapsularli.

 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; } }

Quindi ApiValidationError è una classe che estende ApiSubError ed esprime i problemi di convalida riscontrati durante la chiamata REST.

Di seguito, vedrai alcuni esempi di risposte JSON che vengono generate dopo aver implementato i miglioramenti descritti qui, solo per avere un'idea di ciò che avremo entro la fine di questo articolo.

Ecco un esempio di JSON restituito quando un'entità non viene trovata durante la chiamata all'endpoint GET /birds/2 :

 { "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }

Ecco un altro esempio di JSON restituito quando si emette una chiamata POST /birds con un valore non valido per la massa dell'uccello:

 { "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" } ] } }

Gestione degli errori di avvio a molla

Esaminiamo alcune delle annotazioni di primavera che verranno utilizzate per gestire le eccezioni.

RestController è l'annotazione di base per le classi che gestiscono le operazioni REST.

ExceptionHandler è un'annotazione Spring che fornisce un meccanismo per trattare le eccezioni generate durante l'esecuzione dei gestori (operazioni del controller). Questa annotazione, se utilizzata sui metodi delle classi controller, fungerà da punto di ingresso per la gestione delle eccezioni generate solo all'interno di questo controller. Complessivamente, il modo più comune consiste nell'usare @ExceptionHandler sui metodi delle classi @ControllerAdvice in modo che la gestione delle eccezioni venga applicata a livello globale oa un sottoinsieme di controller.

ControllerAdvice è un'annotazione introdotta nella primavera 3.2 e, come suggerisce il nome, è "Advice" per più controller. Viene utilizzato per consentire l'applicazione di un unico ExceptionHandler a più controller. In questo modo possiamo in un solo posto definire come trattare tale eccezione e questo gestore verrà chiamato quando l'eccezione viene generata dalle classi coperte da questo ControllerAdvice . Il sottoinsieme di controller interessati può essere definito utilizzando i seguenti selettori su @ControllerAdvice : annotations() , basePackageClasses() e basePackages() . Se non vengono forniti selettori, ControllerAdvice viene applicato globalmente a tutti i controller.

Quindi, usando @ExceptionHandler e @ControllerAdvice , saremo in grado di definire un punto centrale per trattare le eccezioni e racchiuderle in un oggetto ApiError con un'organizzazione migliore rispetto al meccanismo di gestione degli errori Spring Boot predefinito.

Gestione delle eccezioni

Rappresentazione di ciò che accade con una chiamata client REST riuscita e non riuscita

Il passaggio successivo consiste nel creare la classe che gestirà le eccezioni. Per semplicità, lo chiamiamo RestExceptionHandler e deve estendersi da ResponseEntityExceptionHandler di Spring Boot. Estenderemo ResponseEntityExceptionHandler poiché fornisce già una gestione di base delle eccezioni Spring MVC, quindi aggiungeremo gestori per nuove eccezioni migliorando al contempo quelle esistenti.

Override delle eccezioni gestite in ResponseEntityExceptionHandler

Se dai un'occhiata al codice sorgente di ResponseEntityExceptionHandler , vedrai molti metodi chiamati handle******() come handleHttpMessageNotReadable() o handleHttpMessageNotWritable() . Vediamo prima come possiamo estendere handleHttpMessageNotReadable() per gestire le eccezioni HttpMessageNotReadableException . Dobbiamo solo sovrascrivere il metodo handleHttpMessageNotReadable() nella nostra classe 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 }

Abbiamo dichiarato che nel caso in cui venga generata HttpMessageNotReadableException , il messaggio di errore sarà "Richiesta JSON malformata" e l'errore verrà incapsulato all'interno dell'oggetto ApiError . Di seguito possiamo vedere la risposta di una chiamata REST con questo nuovo metodo sovrascritto:

 { "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]" } }

Gestione delle eccezioni personalizzate

Ora vedremo come creare un metodo che gestisca un'eccezione che non è stata ancora dichiarata all'interno di ResponseEntityExceptionHandler di Spring Boot.

Uno scenario comune per un'applicazione Spring che gestisce le chiamate al database è avere una chiamata per trovare un record in base al suo ID utilizzando una classe di repository. Ma se esaminiamo il metodo CrudRepository.findOne() , vedremo che restituisce null se non viene trovato un oggetto. Ciò significa che se il nostro servizio chiama semplicemente questo metodo e ritorna direttamente al controller, otterremo un codice HTTP 200 (OK) anche se la risorsa non viene trovata. In effetti, l'approccio corretto consiste nel restituire un codice HTTP 404 (NON TROVATO) come specificato nelle specifiche HTTP/1.1.

Per gestire questo caso, creeremo un'eccezione personalizzata denominata EntityNotFoundException . Questa è un'eccezione creata su misura e diversa da javax.persistence.EntityNotFoundException , in quanto fornisce alcuni costruttori che facilitano la creazione di oggetti e si può scegliere di gestire l'eccezione javax.persistence in modo diverso.

Esempio di chiamata REST non riuscita

Detto questo, creiamo un ExceptionHandler per questa EntityNotFoundException appena creata nella nostra classe RestExceptionHandler . Per fare ciò, crea un metodo chiamato handleEntityNotFound() e annotalo con @ExceptionHandler , passandogli l'oggetto di classe EntityNotFoundException.class . Questo segnala a Spring che ogni volta che viene generata EntityNotFoundException , Spring dovrebbe chiamare questo metodo per gestirlo. Quando si annota un metodo con @ExceptionHandler , accetterà un'ampia gamma di parametri inseriti automaticamente come WebRequest , Locale e altri come descritto qui. Forniremo semplicemente l'eccezione EntityNotFoundException stessa come parametro per questo metodo 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); } }

Grande! Nel metodo handleEntityNotFound() , stiamo impostando il codice di stato HTTP su NOT_FOUND e stiamo usando il nuovo messaggio di eccezione. Ecco come appare ora la risposta per l'endpoint GET /birds/2 :

 { "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }

Conclusione

È importante avere il controllo della gestione delle eccezioni in modo da poter mappare correttamente tali eccezioni all'oggetto ApiError e fornire informazioni importanti che consentono ai client API di sapere cosa è successo. Il passaggio successivo da qui sarebbe creare più metodi di gestione (quelli con @ExceptionHandler) per le eccezioni generate all'interno del codice dell'applicazione. Ci sono più esempi per alcune altre eccezioni comuni come MethodArgumentTypeMismatchException , ConstraintViolationException e altri nel codice GitHub.

Ecco alcune risorse aggiuntive che hanno aiutato nella composizione di questo articolo:

  • Baeldung - Gestione degli errori per REST con Spring

  • Blog di primavera - Gestione delle eccezioni in Spring MVC