Introduction
Si vous travaillez sur une API Spring au quotidien, vous pouvez trouver rébarbatif le fait de renseigner tous les codes HTTP correspondants aux différents cas d’erreur.
On peut les gérer cette façon:
@PostMapping
public ResponseEntity save(@RequestBody Kata kata){
try {
return new ResponseEntity<>(kataService.save(kata), HttpStatus.OK);
}catch(DataIntegrityViolationException exception){
return new ResponseEntity<>( exception.getMessage(), HttpStatus.FORBIDDEN);
}
}
Si on veut plusieurs codes retours en fonction des différentes exceptions possibles alors on peut se retrouver avec beaucoup de catch
dans nos contrôleur.
La promesse de Spring est pourtant de s’occuper de la « plomberie » pour qu’on puisse se concentrer uniquement sur la résolution de problématiques métier.🤔
On va donc voir dans cet article comment avoir des codes retours clairs gérés de façon centralisée.
Cas pratique
Je veux un code de retour 403 et le message de l’exception en réponse dès qu’une exception de type DataIntegrityException est throw dans mes contrôleurs.
Le scénario Cucumber à faire passer:
Scenario: A user should not be allowed to publish a Kata which already exists
Given A user publish a new Kata named AperoTech
And A user publish a new Kata named AperoTech
Then He should have a forbidden response
L’annotation @ExceptionHandler
Le principe est très simple: on va déterminer une réponse dès que certaines exceptions seront levées.
Voici une méthode annotée de @ExceptionHandler qui permet de répondre à la problématique.
@ExceptionHandler({DataIntegrityViolationException.class})
@ResponseStatus(HttpStatus.FORBIDDEN)
public String handleAccessDeniedException(
Exception ex) {
return "DataIntegrity Violation:" + ex.getMessage();
}
Dans l’annotation @ExceptionHandler
, on va mettre la liste des exceptions qui vont être interceptées.
On spécifie un code HTTP de la réponse qui sera renvoyé grâce à l’annotation @ResponseStatus
.
Il ne nous reste plus qu’à spécifier le message d’erreur.
La classe annotée @ControllerAdvice
On peut tout à fait placer le code de l’@ExceptionHandler
dans notre contrôleur et ça fonctionnera. L’exception DataIntegrityViolationException
sera bien interceptée dans le contrôleur.
Notre objectif est d’avoir une classe spécialisée dans la gestion de ces exceptions afin de pouvoir les mutualiser.
Pour se faire, on va créer une nouvelle classe annotée @RestControllerAdvice
(ça revient au même que de l’annoter avec @ControllerAdvice et @ResponseBody)
@ControllerAdvice
est l’annotation qui va nous permettre d’utiliser des méthodes annotées avec @ExceptionHandler
@ResponseBody
va nous permettre de former notre réponse de façon implicite
Notre classe qui va gérer nos exceptions:
package com.aperotech.kata.spring;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler({DataIntegrityViolationException.class})
@ResponseStatus(HttpStatus.FORBIDDEN)
public String handleAccessDeniedException(
Exception ex) {
return "DataIntegrity Violation:" + ex.getMessage();
}
}
Le contrôleur n’a plus besoin de gérer les codes d’erreur, on a un code plus concis:
@PostMapping
public Kata save(@RequestBody Kata kata){
return kataService.save(kata);
}
Conclusion
Grâce à notre classe qui gère les exceptions de façon centralisée, on va gagner en cohérence dans notre application. En effet, un consommateur de notre API n’aura pas de codes de retours HTTP différents pour un même problème.
De plus, on réduit le risque d’oublier de gérer une exception et d’avoir une 500 avec un message d’erreur peu parlant.
Enfin, nos contrôleurs ne font plus que de l’orchestration de service, ils ne se chargent plus de la construction de la réponse qui est externalisée. Ils sont donc plus facile à comprendre.
Vous pouvez bien entendu avoir plusieurs classes spécialisées afin de ne pas avoir une classe “ExceptionHandler” qui devienne trop grosse 🙂