Quelques explications sur l’annotation @RestControllerAdvice.
@RestControllerAdvice
@RestControllerAdvice(basePackages = "fr.chocolaterie.controller.api")
Rôle : Cette annotation combine @ControllerAdvice et @ResponseBody. Elle permet de centraliser la gestion des exceptions pour tous les contrôleurs REST dans un package spécifique comme dans notre exemple dans (fr.chocolaterie.controller.api).
@ControllerAdvice
@ResponseBody
Fonctionnalités :
Chaque méthode est annotée avec @ExceptionHandler et gère un type spécifique d’exception.
@ExceptionHandler
@ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<Map<String, String>> handleIllegalArgument(IllegalArgumentException ex) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Map.of("error", ex.getMessage())); }
L’annotation @ExceptionHandler indique que cette méthode gère les exceptions de type IllegalArgumentException.
IllegalArgumentException
ResponseEntity permet de construire une réponse HTTP avec un statut et un corps.
ResponseEntity
HttpStatus.BAD_REQUEST (400)indique le statut HTTP précisant que la requête est malformée (ex: argument invalide).
HttpStatus.BAD_REQUEST (400)
Corps de la réponse : Une Map avec une clé “error” et la valeur du message d’erreur (ex.getMessage()).
Cas d’utilisation : Exemple : Un utilisateur envoie un ID négatif ou une date invalide.
@ExceptionHandler(DataIntegrityViolationException.class) public ResponseEntity<Map<String, String>> handleDataIntegrity(DataIntegrityViolationException ex) { return ResponseEntity.status(HttpStatus.CONFLICT) .body(Map.of("error", "Opération impossible : la donnée est référencée ailleurs ou viole une contrainte.")); }
DataIntegrityViolationException : Exception lancée par Spring Data/JPA lorsqu’une contrainte de base de données est violée (ex: clé étrangère, unicité, non-null).
DataIntegrityViolationException
HttpStatus.CONFLICT (409) : Statut HTTP indiquant un conflit (ex: tentative de suppression d’une donnée référencée ailleurs).
HttpStatus.CONFLICT (409)
Message personnalisé : Le message est statique (pas ex.getMessage()), car les messages SQL natifs sont souvent techniques et peu clairs pour l’utilisateur.
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) { Map<String, String> fieldErrors = new LinkedHashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> fieldErrors.put(error.getField(), error.getDefaultMessage()) ); Map<String, Object> payload = new LinkedHashMap<>(); payload.put("error", "Validation échouée"); payload.put("fields", fieldErrors); return ResponseEntity.badRequest().body(payload); }
MethodArgumentNotValidException : Lancée par Spring lors de la validation des arguments d’une méthode (ex: avec @Valid).
MethodArgumentNotValidException
BindingResult : Contient les erreurs de validation (ex: champs manquants, valeurs invalides).
BindingResult
getFieldErrors() : Récupère la liste des erreurs pour chaque champ du formulaire/objet.
getFieldErrors()
Structure de la réponse :
Une Map avec :
LinkedHashMap : Conserve l’ordre d’insertion des erreurs (utile pour l’affichage).
Cas d’utilisation : Validation d’un DTO (Data Transfer Object) avec des annotations comme @NotNull, @Size, etc.
@ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleGeneric(Exception ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("error", ex.getMessage() == null ? "Erreur interne." : ex.getMessage())); }
@ExceptionHandler(Exception.class) : Attrape toutes les exceptions non gérées par les handlers précédents.
@ExceptionHandler(Exception.class)
HttpStatus.INTERNAL_SERVER_ERROR (500) : Statut HTTP pour les erreurs internes du serveur.
HttpStatus.INTERNAL_SERVER_ERROR (500)
Message conditionnel : Si ex.getMessage() est null, utilise un message générique (“Erreur interne.”).
Cas d’utilisation : Erreurs inattendues (ex: NullPointerException, problèmes de connexion à la base de données).
Voici comment un contrôleur REST pourrait déclencher ces exceptions :
@RestController @RequestMapping("/api/produits") public class ProduitController { @Autowired private ProduitService produitService; @PostMapping public ResponseEntity<Produit> creerProduit(@Valid @RequestBody ProduitDto produitDto) { // Si produitDto est invalide, MethodArgumentNotValidException est lancée automatiquement par Spring. Produit produit = produitService.creerProduit(produitDto); return ResponseEntity.ok(produit); } @DeleteMapping("/{id}") public ResponseEntity<Void> supprimerProduit(@PathVariable Long id) { if (id <= 0) { throw new IllegalArgumentException("L'ID du produit doit être positif."); } produitService.supprimerProduit(id); // Peut lancer DataIntegrityViolationException return ResponseEntity.noContent().build(); } }
Corps :
{ "error": "Validation échouée", "fields": { "nom": "Le nom est obligatoire" } }
Ne pas exposer les détails internes :
Loguer les erreurs en ajoutant des logs pour les exceptions (avec SLF4J) :
@ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleGeneric(Exception ex) { log.error("Erreur inattendue", ex); // Log pour le débogage return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("error", "Erreur interne.")); }
Statuts HTTP appropriés :
Utilise les statuts HTTP standard pour indiquer le type d’erreur (quoique pour CA-Titres c’est différent) :
400 : Mauvaise requête (validation). 404 : Ressource non trouvée. 409 : Conflit (ex: violation de contrainte). 500 : Erreur serveur.
Documenter les erreurs : Documenter les réponses d’erreur dans l’API (ex: avec Swagger/OpenAPI).
Utiliser MessageSource pour des messages d’erreur traduits :
@Autowired private MessageSource messageSource; @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex, Locale locale) { String globalError = messageSource.getMessage("validation.failed", null, locale); // ... }
Inclure l’heure de l’erreur dans la réponse :
Map<String, Object> payload = new LinkedHashMap<>(); payload.put("timestamp", LocalDateTime.now()); payload.put("error", "Validation échouée");
Retourner plus de détails en développement :
@Value("${spring.profiles.active:prod}") private String activeProfile; @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleGeneric(Exception ex) { if ("dev".equals(activeProfile)) { return ResponseEntity.internalServerError().body(Map.of("error", ex.getMessage(), "stackTrace", getStackTrace(ex))); } else { return ResponseEntity.internalServerError().body(Map.of("error", "Erreur interne.")); } }
Messages clairs