Tutoriel pour gérer ses exceptions web avec les applications Spring

Gokan EKINCI

Ce tutoriel s'adresse à tous ceux qui souhaitent découvrir la gestion des exceptions personnalisées dans les applications Web développées avec le framework Spring.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum.

Commentez

- Titre : Tutoriel pour gérer ses exceptions web avec les applications Spring

- Auteur : Gokan EKINCI

- Parution : 14 mai 2017

- Mise à jour : 12 septembre 2017

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2017 Gokan EKINCI. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Informations

- II. Les exceptions par défaut de Spring

- III. Les exceptions personnalisées - Théorie

- IV. Les exceptions personnalisées - Pratique

- V. Remerciements

I - Informations

Les sources de ce tutoriel sont présents dans le repository Git suivant : https://github.com/eau-de-la-seine/tutorial-spring-exception.git

Vous aurez besoin de Git, Java 8, Maven 3 si vous souhaitez récupérer et lancer l'application.

N'hésitez donc pas à cloner le repository tutorial-spring-exception sur votre machine à partir de la commande Git ci-dessous :

git clone https://github.com/eau-de-la-seine/tutorial-spring-exception.git

Et à utiliser la commande mvn clean install spring-boot:run pour démarrer le projet. N'hésitez pas à lire le tutoriel Maven 3 si vous souhaitez en apprendre plus sur ce dernier.

Enfin, le projet utilise la dernière version stable de Spring Boot à l'heure où j'écris ce tutoriel, soit la version 1.5.3. L'ensemble des classes et annotations présentées dans ce tutoriel ont été créées entre la version 3.0 et 4.3 de Spring.

II - Les exceptions par défaut de Spring

Par défaut, lorsque votre application lance une exception, Spring vous retournera toujours une réponse HTTP avec un code d'erreur 500. Le corps de la réponse sera présenté sous la forme suivante :

{
"timestamp": 1494770602234,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.NullPointerException",
"message": "No message available",
"path": "/trains/itinerary"
}

Nous pourrions traduire ce message JSON par :

une erreur « 500 » de type « Internal Server Error » a été générée à l'instant « 1494770602234 », celle-ci a été provoquée par une exception de type « java.lang.NullPointerException » lorsque le chemin « /trains/itinerary » a été appelé.

Quelle que soit notre exception, le code d'erreur sera toujours 500, signifiant que le serveur est responsable de cette erreur, mais ce n'est pas toujours vrai, nous pourrions très bien vouloir une erreur qui indique que le client est responsable de cette erreur, soit une erreur 400.

Voici un petit rappel des codes HTTP :

Codes HTTP

Famille

1xx

Information

2xx

Succès

3xx

Redirection

4xx

Erreur client

5xx

Erreur serveur

Notre objectif dans ce tutoriel sera de redéfinir le format de réponse HTTP par défaut pour changer le code HTTP et retourner d'autres informations dans le corps de la réponse.

III - Les exceptions personnalisées - Théorie

Pour retourner des messages HTTP personnalisés lors d'une exception, nous allons créer des méthodes dites « exception handler ». Une méthode « exception handler » permet d'attraper une exception et de retourner une réponse HTTP avec le code et le corps de notre choix.

Pour créer une méthode « exception handler », rien de plus simple, il suffit de créer une méthode annotée de @ExceptionHandlerdans un Controller :

// Ci-dessous, une méthode de notre Controller

@ExceptionHandler(MyException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public MyResponseType myExceptionHandler() {
return new MyResponseType();
}

Nous venons de créer une méthode « exception handler » qui permet d'attraper des exceptions de type MyException et de retourner un message de type MyResponseType avec le code 404 NOT FOUND.

Voici un exemple qui est équivalent à l'exemple ci-dessus :

// Ci-dessous, une méthode de notre Controller

@ExceptionHandler(MyException.class)
public ResponseEntity<MyResponseType> myExceptionHandler() {
return new ResponseEntity<>(new MyResponseType(), HttpStatus.NOT_FOUND);
}

Au lieu d'utiliser l'annotation @ResponseStatus, nous retournons un ResponseEntity<T> qui encapsule notre message et un code HTTP. Le code HTTP ne sera plus fixe comme avant, car il présente l'avantage d'être choisi à l'exécution de la méthode.

Une méthode « exception handler » peut avoir plusieurs paramètres facultatifs (vous trouverez tous les types autorisés dans la Javadoc de l'annotation), voici deux types de paramètres qui vont nous intéresser :

- un type HttpServletRequest : peut servir à récupérer le chemin de la page appelée ;

- un type qui hérite de la classe Exception : peut servir à récupérer le message d'erreur.

// Ci-dessous, une méthode de notre Controller

@ExceptionHandler(MyException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public MyResponseType myExceptionHandler(HttpServletRequest request, MyException exception) {

}

Ces deux paramètres seront automatiquement initialisés par Spring et nous permettront d'obtenir plus d'informations pour le message retourné au client.

Une méthode « exception handler » doit être définie dans un Controller, mais il faut aussi savoir que la portée d'une telle méthode se limite au Controller où elle a été définie. Par conséquent, si un Controller autre que celui où vous avez défini vos méthodes « exception handler » lance une exception, l'exception sera capturée par Spring et vous vous retrouverez avec l'erreur 500 générique présentée dans la partie II de ce tutoriel.

Pour que la portée de nos méthodes « exception handler » soit globale à l'application, nous allons plutôt créer une nouvelle classe annotée de @ControllerAdvice et y regrouper toutes ces méthodes. Cette classe sera liée à tous nos contrôleurs.

Récapitulatif d'une méthode « exception handler »

- Une méthode « exception handler » doit être définie dans un Controller (une classe annotée de @Controller ou @RestController), sa portée sera locale aux exceptions lancées à partir du Controller où elle se situe. Si vous souhaitez que la portée de vos méthodes « exception handler » soit globale à l'application, placez-les dans une classe Controller Advice (une classe annotée de @ControllerAdvice ou @RestControllerAdvice ).

- Les annotations précédées de « Rest » ajoutent juste du sucre syntaxique à votre code en vous permettant d'omettre une utilisation explicite de l'annotation @ResponseBody, en effet : @RestController = @Controller + @ResponseBody.

- L'annotation @ResponseStatus permet de redéfinir le code de votre réponse HTTP, si vous l'oubliez, vous retournerez par défaut un code 200 OK. Si votre code HTTP est choisi à l'exécution de la méthode, retournez plutôt un objetResponseEntity<T> au lieu d'utiliser l'annotation @ResponseStatus.

IV - Les exceptions personnalisées - Pratique

Pour mettre en pratique ce que nous avons vu dans la partie III, nous allons appeler un web service REST qui va nous retourner un itinéraire de train à partir des informations de départ et de destination que nous allons lui renseigner.

Diagramme de séquence

Le web service sera mocké, et retournera aléatoirement :

- scénario OK : un itinéraire ;

- scénario KO - Mauvaise adresse de départ : une exception WrongAddressException ;

- scénario KO - Mauvaise adresse de destination : une exception WrongAddressException ;

- scénario KO - Mauvaise gestion des objets Java : une exception NullPointerException.

Voici le code de notre web service pour retourner un itinéraire :

@RestController
public class TrainsController {

@RequestMapping(value = "/itinerary", method = RequestMethod.GET)
public List<ItineraryItem> getItinerary(
@RequestParam("departure") String departure,
@RequestParam("destination") String destination
) {
List<ItineraryItem> itinerary = getMock(departure, destination);
return itinerary;
}

// ...
}

Voici le procédé que nous allons suivre pour gérer nos exceptions : nous allons d'abord créer une méthode « exception handler » spécifique à chaque type d'exception que nous souhaitons capturer, puis une dernière méthode plus générique pour gérer les exceptions qui ne correspondent à aucun cas que nous avons envisagé. Pour que ces méthodes soient globales à l'application, nous allons les définir dans un Controller Advice.

Voici le code de notre Controller Advice :

@RestControllerAdvice
public class ExceptionHandlerControllerAdvice {
private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

@ExceptionHandler(NullPointerException.class)
public ResponseEntity<ExceptionMessage> nullPointerExceptionHandler(HttpServletRequest request, NullPointerException exception) {
ExceptionMessage message = ExceptionMessage.builder()
.date(LocalDateTime.now().format(formatter))
.path(request.getRequestURI().toString() + "?" + request.getQueryString())
.className(exception.getClass().getName())
.message("Tu veux éviter les null ? N'hésite pas à lire cet article: https://www.developpez.net/forums/blogs/473169-gugelhupf/b2944/java-astuces-eviter-nullpointerexception/")
.build();
return new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(WrongAddressException.class)
public ResponseEntity<ExceptionMessage> wrongAddressExceptionHandler(HttpServletRequest request, WrongAddressException exception) {
ExceptionMessage message = ExceptionMessage.builder()
.date(LocalDateTime.now().format(formatter))
.path(request.getRequestURI().toString() + "?" + request.getQueryString())
.className(exception.getClass().getName())
.message(exception.getMessage())
.build();
return new ResponseEntity<>(message, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ExceptionMessage> genericExceptionHandler(HttpServletRequest request, Exception exception) {
ExceptionMessage message = ExceptionMessage.builder()
.date(LocalDateTime.now().format(formatter))
.path(request.getRequestURI().toString() + "?" + request.getQueryString())
.className(exception.getClass().getName())
.message(exception.getMessage())
.build();
return new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

Le résultat obtenu lorsque nous appelons notre web service :

Le hasard a fait en sorte que notre web service mocké lance le scénario KO « Mauvaise gestion des objets Java ».

Enfin, notez que si vous créez plusieurs méthodes « exception handler » avec la même classe d'exception dans @ExceptionHandler, une erreur se produira non pas au lancement de l'application, mais au Runtime, c'est-à-dire lorsque Spring tentera de résoudre quelle méthode « exception handler » doit être exécutée. Voici un aperçu de l'erreur lorsqu'un conflit est détecté  :

Request processing failed; nested exception is java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped

V - Remerciements

Je tiens à remercier ClaudeLELOUP pour la relecture orthographique de ce tutoriel.