Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Gestion centralisée des erreurs avec @ControllerAdvice

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons mis en place une première ligne de défense essentielle pour notre API en validant les données d'entrée avec la Bean Validation. Nous avons vu comment utiliser @RestControllerAdvice pour intercepter MethodArgumentNotValidException et fournir des retours clairs au client.

Aujourd'hui, nous allons généraliser cette approche. Une application robuste ne gère pas seulement les erreurs de validation, mais aussi les erreurs métier (ex: "ressource non trouvée"), les erreurs techniques inattendues, et plus encore. L'objectif de cette leçon est de centraliser toute la gestion des erreurs avec @ControllerAdvice pour garantir des réponses HTTP cohérentes et professionnelles en toutes circonstances. C'est un pilier de la conception d'API de qualité, qui facilite grandement le travail des développeurs frontend qui consommeront votre backend.

1. Le problème : la gestion des erreurs par défaut

Commençons par observer ce qui se passe lorsqu'une exception non gérée survient dans notre logique. Imaginez que dans votre BookService, une recherche par ID échoue et lève une NullPointerException ou une autre exception générique.

Par défaut, Spring Boot va intercepter cette erreur, mais la réponse envoyée au client est souvent un simple statut 500 Internal Server Error avec un corps JSON basique, ou une "Whitelabel Error Page" si vous accédez via un navigateur.

{
    "timestamp": "2023-10-27T10:30:00.123+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api/v1/books/99"
}

Ce comportement, bien que sécurisé (il ne révèle pas de détails internes), n'est pas idéal. Il ne donne aucune information exploitable au client et manque de cohérence avec les erreurs de validation que nous avons formatées à la leçon précédente.

Apprendre Spring Boot #3 Gestion des Erreurs HTTP & Validation (@RestControllerAdvice, Status ...)

La vidéo "Apprendre Spring Boot #3" de la chaîne JirAWS illustre parfaitement ce point de départ. L'auteur montre comment une exception standard non gérée dans la couche service se propage et aboutit à une réponse 500 par défaut de la part de Spring.

Regardez la vidéo de 06:18 à 11:00. Observez comment le fait de lever une new Exception() dans la méthode du service se traduit par une réponse Internal Server Error dans Postman. C'est le comportement que nous allons améliorer.


2. La solution globale : @RestControllerAdvice

Comme nous l'avons entrevu, l'annotation @RestControllerAdvice est la clé. En l'appliquant à une classe, vous signalez à Spring que cette classe contient des conseils (advice) globaux pour tous vos @RestController. L'un de ces "conseils" les plus puissants est la capacité à gérer les exceptions de manière centralisée.

Un gestionnaire d'exceptions global (ou Global Exception Handler) est une classe unique qui va intercepter les exceptions levées depuis n'importe quel point de votre application (contrôleurs, services...) et les transformer en réponses ResponseEntity standardisées.

Spring Boot REST API Exception Handling Flow
Ce diagramme illustre le flux : une exception levée dans la couche Service est interceptée par le `GlobalExceptionHandler`, qui se charge de formater une réponse cohérente pour le client.

La vidéo suivante introduit la mise en place de cette classe.

Apprendre Spring Boot #3 Gestion des Erreurs HTTP & Validation (@RestControllerAdvice, Status ...)

Reprenons la vidéo de JirAWS. L'auteur met maintenant en place la structure de base d'un GlobalExceptionHandler avec @RestControllerAdvice.

Regardez de 11:00 à 15:14. Concentrez-vous sur la création de la classe GlobalExceptionHandler et l'ajout de l'annotation @RestControllerAdvice. L'auteur ajoute une première méthode de gestion qui, pour l'instant, intercepte toutes les exceptions, montrant une première amélioration de la structure de la réponse.


3. Gérer des erreurs métier avec des exceptions personnalisées

Le véritable pouvoir de cette approche réside dans sa capacité à gérer différents types d'erreurs de manière spécifique. Une "ressource non trouvée" (erreur 404) n'est pas la même chose qu'une "requête invalide" (erreur 400).

Pour y parvenir, nous suivons une démarche en 3 étapes :

Étape 1 : Créer une exception métier personnalisée

Plutôt que de lever des exceptions génériques comme RuntimeException, nous créons nos propres classes d'exception qui décrivent précisément l'erreur métier. Cela rend votre code de service plus expressif et le découple des détails de l'API REST.

// Dans un package, par exemple com.example.project.exception
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Étape 2 : Lever l'exception depuis la couche service

Dans votre service, lorsque la condition d'erreur est rencontrée, vous levez votre nouvelle exception.

@Service
public class BookService {
    // ...
    public Book findBookById(Long id) {
        return bookRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Le livre avec l'ID " + id + " n'a pas été trouvé."));
    }
}

Votre BookService signale clairement un problème métier, sans se soucier de savoir si cela deviendra une erreur HTTP 404 ou sera géré autrement.

Étape 3 : Créer un @ExceptionHandler spécifique

Dans votre classe @RestControllerAdvice, vous ajoutez une méthode pour intercepter spécifiquement cette exception.

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException ex) {
        // Logique pour formater une réponse 404
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", ex.getMessage());

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

    // ... autres handlers
}

L'annotation @ExceptionHandler(ResourceNotFoundException.class) indique à Spring d'invoquer cette méthode uniquement lorsqu'une ResourceNotFoundException (ou une de ses sous-classes) est levée.

Gérez les exceptions de manière cohérente et centralisée dans Spring MVC avec ControllerAdvice

L'article "Gérez les exceptions de manière cohérente et centralisée" de Ntepp Jean Joel détaille cette approche de bout en bout avec un exemple de projet de gestion de livres.

Lisez les sections 5 et 6 de l'article (numérotées dans le texte). La section 5, "Les Exceptions avec ControllerAdvice", montre comment créer les classes d'exception personnalisées (BookNotFoundException) et le GlobalExceptionHandler avec les méthodes @ExceptionHandler. La section 6 montre comment l'implémentation du service (BookServiceImp) lève ces exceptions. C'est un excellent exemple de code complet.


4. Structurer une réponse d'erreur cohérente

Pour une API professionnelle, toutes les réponses d'erreur devraient suivre la même structure JSON. Cela permet au développeur frontend de créer un mécanisme de gestion d'erreurs unique.
Une bonne pratique consiste à créer un Record ou une classe DTO pour représenter cette structure.

// Un record Java est parfait pour cet usage : immuable et concis.
public record ErrorResponse(
    LocalDateTime timestamp,
    int status,
    String error,
    String message,
    String path
) {}

Votre @ExceptionHandler utiliserait alors ce record pour construire la réponse :

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) {
    ErrorResponse errorResponse = new ErrorResponse(
        LocalDateTime.now(),
        HttpStatus.NOT_FOUND.value(),
        HttpStatus.NOT_FOUND.getReasonPhrase(),
        ex.getMessage(),
        ((ServletWebRequest)request).getRequest().getRequestURI()
    );
    return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}

Le client recevrait alors une réponse claire et structurée :

{
    "timestamp": "2023-10-27T10:35:12.456Z",
    "status": 404,
    "error": "Not Found",
    "message": "Le livre avec l'ID 99 n'a pas été trouvé.",
    "path": "/api/v1/books/99"
}
API Error Response Examples in Swagger UI
Cette image, tirée d'une documentation Swagger, montre différents types de réponses d'erreur (400, 401, 422, 500) qui suivent toutes une structure `problem+json` standardisée. C'est le niveau de professionnalisme que nous visons.

5. La sécurité : le gestionnaire "fourre-tout"

Que se passe-t-il si une exception que vous n'aviez pas prévue survient ? Une NullPointerException inattendue, une erreur de connexion à un service tiers... ? Nous devons absolument éviter que la trace de la pile (stack trace) de Java ne soit exposée au client.

Pour cela, nous ajoutons un "filet de sécurité" : un ExceptionHandler qui intercepte l'exception la plus générique possible, comme Exception.class.

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex, WebRequest request) {
    // IMPORTANT: Logguez l'exception complète côté serveur pour le débogage.
    // log.error("Une erreur inattendue est survenue", ex);

    ErrorResponse errorResponse = new ErrorResponse(
        LocalDateTime.now(),
        HttpStatus.INTERNAL_SERVER_ERROR.value(),
        HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
        "Une erreur interne est survenue. Veuillez réessayer plus tard.", // Message générique
        ((ServletWebRequest)request).getRequest().getRequestURI()
    );
    return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); // Toujours 500
}

Grâce à la hiérarchie des exceptions en Java, Spring cherchera toujours le gestionnaire le plus spécifique en premier (ResourceNotFoundException). Si aucun ne correspond, il se rabattra sur ce gestionnaire générique.

Apprendre Spring Boot #3 Gestion des Erreurs HTTP & Validation (@RestControllerAdvice, Status ...)

Pour bien comprendre pourquoi cette approche est si robuste, il est utile de revoir la hiérarchie des exceptions en Java. La vidéo de JirAWS contient une explication théorique mais essentielle sur le sujet.

Regardez la section de 20:56 à 34:03. L'auteur explique la différence entre Throwable, Error et Exception. Il clarifie comment Spring, pour garantir la stabilité, "enveloppe" toutes les erreurs dans des exceptions. Cela garantit que votre gestionnaire ExceptionHandler(Exception.class) agira bien comme un filet de sécurité pour toute erreur survenant dans votre application.

Ce mécanisme garantit que votre API retourne toujours une réponse JSON formatée, même en cas de problème grave et inattendu, ce qui est la marque d'une application de production robuste.


Conclusion

Félicitations ! Vous avez transformé la gestion des erreurs de votre API, la faisant passer d'un comportement par défaut à un système centralisé, cohérent et professionnel.

Points clés à retenir :

  • L'annotation @RestControllerAdvice permet de créer une classe qui centralise la gestion des exceptions pour tous vos contrôleurs.
  • Chaque méthode annotée avec @ExceptionHandler au sein de cette classe cible une exception spécifique.
  • Il est recommandé de créer des exceptions personnalisées (ex: ResourceNotFoundException) pour représenter les erreurs métier, ce qui rend votre code de service plus propre et expressif.
  • La création d'un DTO/Record d'erreur standard (ex: ErrorResponse) assure que toutes les réponses d'erreur de votre API ont une structure JSON cohérente.
  • Un gestionnaire "fourre-tout" pour Exception.class est un filet de sécurité indispensable pour intercepter les erreurs inattendues et renvoyer une réponse 500 Internal Server Error propre, sans jamais exposer de détails d'implémentation.

Maintenant que la fondation de notre API est solide (configuration par profils, validation des entrées et gestion globale des erreurs), nous sommes prêts à aborder le cœur de notre application : la persistance des données.

Dans la prochaine leçon, nous plongerons dans le module Spring Data JPA pour connecter notre application à une base de données MySQL et gérer nos entités de manière efficace.