Guide de gestion des erreurs de l'API Spring Boot REST
Publié: 2022-03-11La gestion correcte des erreurs dans les API tout en fournissant des messages d'erreur significatifs est une fonctionnalité très souhaitable, car elle peut aider le client API à répondre correctement aux problèmes. Le comportement par défaut a tendance à renvoyer des traces de pile difficiles à comprendre et finalement inutiles pour le client API. Le partitionnement des informations d'erreur dans des champs permet également au client API de les analyser et de fournir de meilleurs messages d'erreur à l'utilisateur. Dans cet article, nous expliquerons comment gérer correctement les erreurs lors de la création d'une API REST avec Spring Boot.
La création d'API REST avec Spring est devenue l'approche standard pour les développeurs Java au cours des deux dernières années. L'utilisation de Spring Boot aide considérablement, car elle supprime une grande partie du code passe-partout et permet la configuration automatique de divers composants. Nous supposerons que vous connaissez les bases du développement d'API avec ces technologies avant d'appliquer les connaissances décrites ici. Si vous ne savez toujours pas comment développer une API REST de base, vous devriez commencer par cet article sur Spring MVC ou un autre sur la création d'un service Spring REST.
Rendre les réponses d'erreur plus claires
Tout au long de cet article, nous utiliserons le code source hébergé sur GitHub d'une application qui implémente une API REST pour récupérer des objets représentant des oiseaux. Il possède les fonctionnalités décrites dans cet article et quelques exemples supplémentaires de scénarios de gestion des erreurs. Voici un récapitulatif des points de terminaison mis en œuvre dans cette application :
GET /birds/{birdId} | Obtient des informations sur un oiseau et lève une exception s'il n'est pas trouvé. |
GET /birds/noexception/{birdId} | Cet appel obtient également des informations sur un oiseau, sauf qu'il ne lève pas d'exception au cas où l'oiseau ne serait pas trouvé. | POST /birds | Crée un oiseau. | </tr>
Le module Spring framework MVC est livré avec quelques fonctionnalités intéressantes pour aider à la gestion des erreurs. Mais il appartient au développeur d'utiliser ces fonctionnalités pour traiter les exceptions et renvoyer des réponses significatives au client API.
Regardons un exemple de la réponse Spring Boot par défaut lorsque nous émettons un HTTP POST au point de terminaison /birds avec l'objet JSON suivant, qui a la chaîne « aaa » sur le champ « mass », qui devrait attendre un entier :
{ "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }La réponse par défaut de Spring Boot, sans gestion appropriée des erreurs :
{ "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" } Eh bien… le message de réponse contient de bons champs, mais il se concentre trop sur ce qu'était l'exception. Soit dit en passant, il s'agit de la classe DefaultErrorAttributes de Spring Boot. Le champ d' timestamp est un nombre entier qui ne contient même pas d'informations sur l'unité de mesure dans laquelle se trouve l'horodatage. Le champ d' exception n'intéresse que les développeurs Java et le message laisse le consommateur d'API perdu dans tous les détails d'implémentation qui ne sont pas pertinents pour eux. . Et s'il y avait plus de détails que nous pourrions extraire de l'exception à l'origine de l'erreur ? Apprenons donc à traiter correctement ces exceptions et à les encapsuler dans une représentation JSON plus agréable pour faciliter la vie de nos clients API.
Comme nous allons utiliser les classes de date et d'heure Java 8, nous devons d'abord ajouter une dépendance Maven pour les convertisseurs Jackson JSR310. Ils se chargent de convertir les classes de date et d'heure Java 8 en représentation JSON à l'aide de l'annotation @JsonFormat :
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> Ok, alors définissons une classe pour représenter les erreurs d'API. Nous allons créer une classe appelée ApiError qui contient suffisamment de champs pour contenir des informations pertinentes sur les erreurs qui se produisent lors des appels 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 propriété
statuscontient le statut de l'appel de l'opération. Ce sera quelque chose de 4xx pour signaler les erreurs du client ou 5xx pour signifier les erreurs du serveur. Un scénario courant est un code http 400 qui signifie un BAD_REQUEST, lorsque le client, par exemple, envoie un champ mal formaté, comme une adresse e-mail invalide.La propriété
timestampcontient l'instance date-heure du moment où l'erreur s'est produite.La propriété
messagecontient un message convivial sur l'erreur.La propriété
debugMessagecontient un message système décrivant l'erreur plus en détail.La propriété
subErrorscontient un tableau des sous-erreurs qui se sont produites. Ceci est utilisé pour représenter plusieurs erreurs dans un seul appel. Un exemple serait des erreurs de validation dans lesquelles plusieurs champs ont échoué à la validation. La classeApiSubErrorest utilisée pour les encapsuler.
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; } } Ainsi, ApiValidationError est une classe qui étend ApiSubError et exprime les problèmes de validation rencontrés lors de l'appel REST.
Ci-dessous, vous verrez quelques exemples de réponses JSON qui sont générées après que nous ayons implémenté les améliorations décrites ici, juste pour avoir une idée de ce que nous aurons à la fin de cet article.
Voici un exemple de JSON renvoyé lorsqu'une entité n'est pas trouvée lors de l'appel du point de terminaison GET /birds/2 :
{ "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } } Voici un autre exemple de JSON renvoyé lors de l'émission d'un appel POST /birds avec une valeur invalide pour la masse de l'oiseau :
{ "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" } ] } }Gestion des erreurs de démarrage du printemps
Explorons quelques-unes des annotations Spring qui seront utilisées pour gérer les exceptions.

RestController est l'annotation de base pour les classes qui gèrent les opérations REST.
ExceptionHandler est une annotation Spring qui fournit un mécanisme pour traiter les exceptions levées lors de l'exécution des gestionnaires (opérations du contrôleur). Cette annotation, si elle est utilisée sur les méthodes des classes de contrôleur, servira de point d'entrée pour gérer les exceptions levées dans ce contrôleur uniquement. Dans l'ensemble, la méthode la plus courante consiste à utiliser @ExceptionHandler sur les méthodes des classes @ControllerAdvice afin que la gestion des exceptions soit appliquée globalement ou à un sous-ensemble de contrôleurs.
ControllerAdvice est une annotation introduite dans Spring 3.2 et, comme son nom l'indique, est un "conseil" pour plusieurs contrôleurs. Il est utilisé pour permettre à un seul ExceptionHandler d'être appliqué à plusieurs contrôleurs. De cette façon, nous pouvons en un seul endroit définir comment traiter une telle exception et ce gestionnaire sera appelé lorsque l'exception est levée à partir de classes couvertes par ce ControllerAdvice . Le sous-ensemble de contrôleurs concernés peut être défini à l'aide des sélecteurs suivants sur @ControllerAdvice : annotations() , basePackageClasses() et basePackages() . Si aucun sélecteur n'est fourni, le ControllerAdvice est appliqué globalement à tous les contrôleurs.
Ainsi, en utilisant @ExceptionHandler et @ControllerAdvice , nous pourrons définir un point central pour traiter les exceptions et les encapsuler dans un objet ApiError avec une meilleure organisation que le mécanisme de gestion des erreurs Spring Boot par défaut.
Gestion des exceptions
L'étape suivante consiste à créer la classe qui gérera les exceptions. Pour plus de simplicité, nous l'appelons RestExceptionHandler et il doit s'étendre à partir de ResponseEntityExceptionHandler de Spring Boot. Nous allons étendre ResponseEntityExceptionHandler car il fournit déjà une gestion de base des exceptions Spring MVC, nous allons donc ajouter des gestionnaires pour les nouvelles exceptions tout en améliorant celles existantes.
Remplacement des exceptions gérées dans ResponseEntityExceptionHandler
Si vous jetez un œil au code source de ResponseEntityExceptionHandler , vous verrez de nombreuses méthodes appelées handle******() comme handleHttpMessageNotReadable() ou handleHttpMessageNotWritable() . Voyons d'abord comment étendre handleHttpMessageNotReadable() pour gérer les exceptions HttpMessageNotReadableException . Nous devons juste remplacer la méthode handleHttpMessageNotReadable() dans notre 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 } Nous avons déclaré qu'en cas de levée d'une HttpMessageNotReadableException , le message d'erreur sera "Requête JSON malformée" et l'erreur sera encapsulée dans l'objet ApiError . Ci-dessous, nous pouvons voir la réponse d'un appel REST avec cette nouvelle méthode remplacée :
{ "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]" } }Gestion des exceptions personnalisées
Nous allons maintenant voir comment créer une méthode qui gère une exception qui n'est pas encore déclarée dans ResponseEntityExceptionHandler de Spring Boot.
Un scénario courant pour une application Spring qui gère les appels de base de données consiste à avoir un appel pour rechercher un enregistrement par son ID à l'aide d'une classe de référentiel. Mais si nous examinons la méthode CrudRepository.findOne() , nous verrons qu'elle renvoie null si un objet n'est pas trouvé. Cela signifie que si notre service appelle simplement cette méthode et retourne directement au contrôleur, nous obtiendrons un code HTTP 200 (OK) même si la ressource n'est pas trouvée. En fait, l'approche appropriée consiste à renvoyer un code HTTP 404 (NOT FOUND) comme spécifié dans la spécification HTTP/1.1.
Pour gérer ce cas, nous allons créer une exception personnalisée appelée EntityNotFoundException . Celui-ci est une exception créée sur mesure et différente de javax.persistence.EntityNotFoundException , car il fournit des constructeurs qui facilitent la création d'objets, et on peut choisir de gérer l'exception javax.persistence différemment.
Cela dit, créons un ExceptionHandler pour cette EntityNotFoundException nouvellement créée dans notre classe RestExceptionHandler . Pour ce faire, créez une méthode appelée handleEntityNotFound() et annotez-la avec @ExceptionHandler , en lui transmettant l'objet de classe EntityNotFoundException.class . Cela signale à Spring que chaque fois que EntityNotFoundException est lancée, Spring doit appeler cette méthode pour la gérer. Lors de l'annotation d'une méthode avec @ExceptionHandler , elle acceptera un large éventail de paramètres auto-injectés tels que WebRequest , Locale et autres, comme décrit ici. Nous fournirons simplement l'exception EntityNotFoundException elle-même en tant que paramètre pour cette méthode 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); } } Génial! Dans la méthode handleEntityNotFound() , nous définissons le code d'état HTTP sur NOT_FOUND et utilisons le nouveau message d'exception. Voici à quoi ressemble maintenant la réponse pour le point de terminaison GET /birds/2 :
{ "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }Conclusion
Il est important de contrôler la gestion des exceptions afin que nous puissions correctement mapper ces exceptions à l'objet ApiError et fournir des informations importantes permettant aux clients API de savoir ce qui s'est passé. La prochaine étape à partir de là serait de créer plus de méthodes de gestionnaire (celles avec @ExceptionHandler) pour les exceptions levées dans le code de l'application. Il existe d'autres exemples d'exceptions courantes telles que MethodArgumentTypeMismatchException , ConstraintViolationException et d'autres dans le code GitHub.
Voici quelques ressources supplémentaires qui ont aidé à la rédaction de cet article :
Baeldung - Gestion des erreurs pour REST avec Spring
Spring Blog - Gestion des exceptions dans Spring MVC
