Contactez-nous

Gestion globale des exceptions avec `@ControllerAdvice` et `@ExceptionHandler`

Apprenez à implémenter une gestion globale et cohérente des exceptions dans vos applications Spring Boot en utilisant @ControllerAdvice et @ExceptionHandler pour des API plus robustes.

Le problème des exceptions non gérées et du code répétitif

Dans toute application, des erreurs peuvent survenir : une ressource demandée n'existe pas, des données d'entrée sont invalides, une dépendance externe échoue, etc. Sans mécanisme de gestion approprié, ces erreurs (exceptions) peuvent remonter jusqu'au conteneur de servlets et entraîner des réponses génériques et peu informatives pour le client (comme une page d'erreur HTML par défaut de Spring Boot ou une trace de pile brute), ou pire, planter la requête.

Une approche naïve consisterait à entourer chaque appel potentiellement problématique dans vos méthodes de contrôleur avec des blocs try-catch. Cependant, cela conduit rapidement à un code de contrôleur volumineux, répétitif, difficile à lire et à maintenir. La logique de gestion des erreurs se retrouve dispersée dans toute l'application, et il devient compliqué d'assurer une réponse d'erreur cohérente pour les clients de votre API.

Spring MVC (et donc Spring Boot) fournit une solution beaucoup plus élégante et centralisée pour gérer les exceptions qui se produisent pendant le traitement des requêtes : les annotations @ControllerAdvice et @ExceptionHandler.

@ControllerAdvice : Un assistant global pour vos contrôleurs

L'annotation @ControllerAdvice est une spécialisation de l'annotation @Component. Elle permet de créer une classe qui contient des méthodes applicables globalement à plusieurs (ou tous) les contrôleurs de votre application. C'est une sorte de "conseiller" ou d'"assistant" pour vos contrôleurs.

Une classe annotée avec @ControllerAdvice peut contenir des méthodes annotées avec :

  • @ExceptionHandler : Pour gérer spécifiquement certains types d'exceptions levées par les méthodes des contrôleurs. C'est le cas d'usage le plus fréquent pour la gestion globale des erreurs.
  • @InitBinder : Pour personnaliser la liaison des données de requête (data binding).
  • @ModelAttribute : Pour ajouter des attributs communs au modèle de toutes les vues rendues par les contrôleurs concernés.

En plaçant la logique de gestion des exceptions dans une classe @ControllerAdvice, vous la sortez de vos contrôleurs individuels et la centralisez en un seul endroit, améliorant ainsi la clarté et la maintenabilité du code.

@ExceptionHandler : Intercepter et traiter des exceptions spécifiques

L'annotation @ExceptionHandler est utilisée sur une méthode (typiquement au sein d'une classe @ControllerAdvice) pour indiquer que cette méthode doit être invoquée lorsqu'une exception d'un type spécifique (ou d'un de ses sous-types) est levée par une méthode de contrôleur (ou un filtre/intercepteur associé) et n'est pas déjà gérée localement par un autre @ExceptionHandler dans le contrôleur lui-même.

La méthode annotée avec @ExceptionHandler peut prendre plusieurs types d'arguments, notamment :

  • L'exception elle-même (par exemple, ResourceNotFoundException ex).
  • La requête HTTP (HttpServletRequest ou WebRequest).
  • La réponse HTTP (HttpServletResponse).

Et surtout, elle peut retourner différents types pour construire la réponse d'erreur :

  • ResponseEntity : Le choix le plus flexible, permettant de contrôler totalement le code de statut, les en-têtes et le corps de la réponse d'erreur (souvent un DTO d'erreur).
  • Un objet (ex: un DTO d'erreur) : Spring utilisera alors @ResponseBody (implicite dans @ControllerAdvice si elle est aussi annotée @RestControllerAdvice) pour sérialiser l'objet et utilisera un code de statut par défaut (souvent 500) ou celui défini par @ResponseStatus sur la méthode handler.
  • ModelAndView : Pour rediriger vers une page d'erreur HTML dans une application MVC traditionnelle.

Mise en oeuvre d'un gestionnaire d'exceptions global

Créons un exemple de classe RestExceptionHandler pour gérer différentes erreurs courantes dans une API REST :

package com.certiquizz.monappliboot.exception;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

// @ControllerAdvice ou @RestControllerAdvice (cette dernière ajoute @ResponseBody implicitement)
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler { // Etendre cette classe aide à gérer les exceptions Spring MVC internes

    // Gestionnaire pour une exception personnalisée "ResourceNotFoundException"
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) {
        Map body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.NOT_FOUND.value());
        body.put("error", "Not Found");
        body.put("message", ex.getMessage());
        body.put("path", request.getDescription(false).replace("uri=", ""));

        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }

    // Gestionnaire pour les erreurs de validation Bean (@Valid)
    // On surcharge la méthode de ResponseEntityExceptionHandler pour personnaliser la réponse
    @Override
    protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        Map body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", status.value()); // Généralement 400 Bad Request
        body.put("error", "Validation Error");

        // Récupérer les erreurs de validation spécifiques
        List errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(x -> x.getField() + ": " + x.getDefaultMessage())
                .collect(Collectors.toList());
        body.put("validationErrors", errors);
        body.put("path", request.getDescription(false).replace("uri=", ""));

        return new ResponseEntity<>(body, headers, status);
    }

    // Gestionnaire générique pour toutes les autres exceptions non capturées
    @ExceptionHandler(Exception.class)
    public ResponseEntity handleGenericException(Exception ex, WebRequest request) {
        // Loguer l'exception est très important ici !
        logger.error("Une erreur inattendue est survenue", ex);

        Map body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        body.put("error", "Internal Server Error");
        // Ne pas exposer les détails internes de l'exception au client en production !
        body.put("message", "Une erreur interne est survenue. Veuillez réessayer plus tard."); 
        body.put("path", request.getDescription(false).replace("uri=", ""));

        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

// Exception personnalisée (exemple)
class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Dans cet exemple :

  • La classe est annotée avec @ControllerAdvice.
  • Elle étend ResponseEntityExceptionHandler pour bénéficier de la gestion par défaut de certaines exceptions Spring MVC internes (comme MethodArgumentNotValidException) tout en nous permettant de personnaliser la réponse.
  • Une méthode handleResourceNotFound est définie pour gérer notre exception métier personnalisée ResourceNotFoundException. Elle retourne une ResponseEntity avec un statut 404 et un corps JSON structuré contenant des détails sur l'erreur.
  • La méthode handleMethodArgumentNotValid est surchargée pour personnaliser la réponse en cas d'échec de validation (via @Valid). Elle extrait les messages d'erreur spécifiques à chaque champ et les inclut dans la réponse 400.
  • Une méthode handleGenericException sert de filet de sécurité pour toutes les autres exceptions non prévues. Elle logue l'erreur (essentiel pour le débogage) et retourne une réponse 500 générique au client, sans fuiter de détails internes.

Structurer la réponse d'erreur

Il est fortement recommandé de définir une structure cohérente pour vos réponses d'erreur JSON. Au lieu d'utiliser une Map générique comme dans l'exemple ci-dessus, créez un DTO spécifique pour les erreurs.

package com.certiquizz.monappliboot.dto.error;

import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@JsonInclude(JsonInclude.Include.NON_NULL) // N'inclut pas les champs null dans le JSON
public class ApiErrorResponse {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;
    private List validationErrors;
    private Map details; // Pour des détails supplémentaires optionnels

    // Constructeurs, Getters, Setters...
    public ApiErrorResponse(HttpStatus status, String message, String path) {
        this.timestamp = LocalDateTime.now();
        this.status = status.value();
        this.error = status.getReasonPhrase();
        this.message = message;
        this.path = path;
    }
     // Constructeur pour erreurs de validation
    public ApiErrorResponse(HttpStatus status, String message, String path, List validationErrors) {
        this(status, message, path);
        this.validationErrors = validationErrors;
    }
    // ... autres constructeurs / méthodes si nécessaire ...
    // Getters/Setters
}

Vous utiliseriez ensuite ce DTO dans vos méthodes @ExceptionHandler :

// ... dans RestExceptionHandler ...
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) {
    String path = request.getDescription(false).replace("uri=", "");
    ApiErrorResponse apiError = new ApiErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage(), path);
    return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND);
}
// ... adapter les autres handlers de la même manière ...

Avantages et conclusion

L'utilisation combinée de @ControllerAdvice et @ExceptionHandler offre une approche robuste et centralisée pour la gestion des erreurs dans les applications Spring Boot :

  • Centralisation : Toute la logique de gestion des erreurs est regroupée.
  • Code de contrôleur propre : Les contrôleurs se concentrent sur leur logique principale sans être pollués par des try-catch répétitifs.
  • Réponses d'erreur cohérentes : Assure que tous les clients reçoivent des réponses d'erreur structurées et uniformes.
  • Séparation des préoccupations : La gestion des erreurs est clairement séparée de la logique métier et de la logique de contrôle.
  • Flexibilité : Permet de gérer différents types d'exceptions de manière spécifique et de personnaliser entièrement la réponse HTTP.

C'est la méthode standard et recommandée pour gérer les exceptions dans les API REST développées avec Spring Boot, contribuant à la création d'applications plus maintenables, robustes et conviviales pour les développeurs consommateurs.