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 @ExceptionHandler
dans 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
.
- 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
- 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.
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.