Aller au contenu

Complément sur certaines annoations dans Spring Boot

@RestControllerAdvice

1. Introduction

Quelques explications sur l’annotation @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).

Fonctionnalités :

2. Méthodes de Gestion des Exceptions

Chaque méthode est annotée avec @ExceptionHandler et gère un type spécifique d’exception.

a. Gestion de IllegalArgumentException

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

ResponseEntity permet de construire une réponse HTTP avec un statut et un corps.

HttpStatus.BAD_REQUEST (400)indique le statut HTTP précisant que la requête est malformée (ex: argument invalide).

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.

b. Gestion de DataIntegrityViolationException

@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).

HttpStatus.CONFLICT (409) : Statut HTTP indiquant un conflit (ex: tentative de suppression d’une donnée référencée ailleurs).

Message personnalisé : Le message est statique (pas ex.getMessage()), car les messages SQL natifs sont souvent techniques et peu clairs pour l’utilisateur.

c. Gestion de MethodArgumentNotValidException

@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).

BindingResult : Contient les erreurs de validation (ex: champs manquants, valeurs invalides).

getFieldErrors() : Récupère la liste des erreurs pour chaque champ du formulaire/objet.

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.

d. Gestion Générique des Exceptions

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

HttpStatus.INTERNAL_SERVER_ERROR (500) : Statut HTTP pour les erreurs internes du serveur.

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).

3. Pourquoi Utiliser @RestControllerAdvice ?

Avantages Exemple
Centralisation des exceptions Toutes les erreurs sont gérées au même endroit.
Réponses HTTP standardisées Format JSON cohérent pour toutes les erreurs.
Séparation des préoccupations La logique métier et la gestion des erreurs sont séparées.
Personnalisation des statuts HTTP 400 pour les erreurs de validation, 409 pour les conflits, etc.
Messages d’erreur clairs Les clients (front-end, applications mobiles) reçoivent des messages compréhensibles.

4. Exemple d’Utilisation dans un Contrôleur

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();
    }
}

5. Déroulement d’une Requête avec Erreur

  1. Requête envoyée : Exemple : POST /api/produits avec un corps JSON invalide (champ nom manquant).
  2. Validation échoue : Spring lance une MethodArgumentNotValidException.
  3. Interception par ApiExceptionHandler : La méthode handleValidation est appelée.
  4. Réponse générée : Statut HTTP : 400 Bad Request.

Corps :

{
  "error": "Validation échouée",
  "fields": {
    "nom": "Le nom est obligatoire"
  }
}

6. Bonnes Pratiques

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).

7. Exemple de Réponse JSON pour Chaque Handler

Exception Statut HTTP Réponse JSON
IllegalArgumentException 400 {“error”: “L’ID doit être positif.”}
DataIntegrityViolationException 409 {“error”: “Opération impossible : la donnée est référencée ailleurs ou viole une contrainte.”}
MethodArgumentNotValidException 400 {“error”: “Validation échouée”, “fields”: {“nom”: “Le nom est obligatoire”}}
Exception (générique) 500 {“error”: “Erreur interne.”}

8. Extensions Possibles

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."));
    }
}

Résumé