Le Kobayashi Maru de Java : La Gestion Globale des Exceptions
Pour ceux qui ont séché les cours de l’Académie de la Flotte Stellaire, le Kobayashi Maru est un exercice d’entraînement conçu comme un “scénario sans issue”. Le but n’est pas de gagner, c’est de voir comment vous gérez un échec inévitable. Dans le monde de l’ingénierie Backend, notre scénario sans issue est l’Exception Non Gérée.
Vous passez des semaines à concevoir un service beau et propre. Vous utilisez des Records, vous optimisez vos requêtes SQL, vous appliquez les principes SOLID. Et puis, le jour de la mise en production, un utilisateur envoie un JSON malformé, et votre API vomit une Stack Trace de 50 lignes directement dans la console de son navigateur. C’est moche, c’est peu professionnel, et cela expose votre logique interne au monde entier.

La manière amateur de gérer cela est de parsemer votre base de code de blocs try-catch. Vous enveloppez chaque méthode de Controller dans une couverture de sécurité, dupliquant le code et rendant votre logique illisible.
Mais puisque nous utilisons Spring Boot, nous pouvons faire ce qu’a fait le Capitaine Kirk : nous pouvons reprogrammer la simulation. Nous n’avons pas à combattre l’exception dans chaque méthode, nous pouvons l’intercepter globalement avant qu’elle ne quitte jamais le sas.
La Stratégie : @ControllerAdvice
Spring fournit une annotation appelée @ControllerAdvice. Voyez-la comme un intercepteur global. Elle se place au-dessus de tous vos contrôleurs, surveillant toute erreur qui remonte à la surface. Si une exception est levée n’importe où dans votre application, ce composant l’attrape, la formate et renvoie une réponse JSON polie au lieu d’une erreur brute.
Mettons en place un mécanisme de gestion d’erreurs propre et centralisé.
Étape 1 : Définir l’Erreur Standard
Premièrement, nous arrêtons de renvoyer des chaînes de caractères ou des Maps aléatoires. Nous avons besoin d’un contrat. Puisque nous sommes sur Java 21, utilisons un record pour créer une structure de données immuable et concise pour nos erreurs.
package me.doismiu.api.exception;
import java.time.LocalDateTime;
public record ApiError(
LocalDateTime timestamp,
int status,
String error,
String path
) {
public ApiError(int status, String error, String path) {
this(LocalDateTime.now(), status, error, path);
}
}
Étape 2 : Le Gestionnaire Global
Maintenant, la magie opère. Nous créons une classe annotée avec @ControllerAdvice. À l’intérieur, nous définissons des méthodes annotées avec @ExceptionHandler pour chaque problème spécifique que nous voulons intercepter.
Je les sépare généralement en deux catégories : Les Erreurs Logiques Attendues (comme “Utilisateur Non Trouvé”) et L’Inattendu (NullPointers, plantages de base de données).
package me.doismiu.api.exception;
import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
// Scénario 1 : L'utilisateur a demandé quelque chose qui n'existe pas.
// Nous renvoyons un 404 (Not Found), pas un 500 (Server Error).
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ApiError> handleEntityNotFound(
EntityNotFoundException ex,
HttpServletRequest request
) {
ApiError error = new ApiError(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
// Scénario 2 : Validation de la Logique Métier (ex : "Fonds Insuffisants")
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiError> handleBadRequest(
IllegalArgumentException ex,
HttpServletRequest request
) {
ApiError error = new ApiError(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
// Scénario 3 : Le "Kobayashi Maru" (Tout le reste)
// Cela attrape les NullPointers, les erreurs SQL, et tout ce que nous n'avons pas prévu.
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGenericException(
Exception ex,
HttpServletRequest request
) {
// Journaliser la vraie stack trace en interne pour pouvoir la corriger plus tard
// Mais ne JAMAIS la montrer au client.
ex.printStackTrace();
ApiError error = new ApiError(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Une erreur inattendue s'est produite. Veuillez contacter le support.",
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Pourquoi C’est Important
En faisant cela, vous découplez la gestion des erreurs de la logique métier. Vos classes Service peuvent simplement throw new EntityNotFoundException("Utilisateur id 5 non trouvé"), et elles n’ont pas à se soucier des statuts HTTP ou du formatage JSON. Le GlobalExceptionHandler s’occupe de la traduction.
Cela transforme un crash chaotique en un atterrissage contrôlé. Le client reçoit un JSON propre expliquant ce qui s’est passé, et votre code reste propre, lisible et concentré sur le scénario nominal.
Vous n’avez pas nécessairement “gagné” (l’erreur s’est quand même produite), mais vous avez changé les règles du jeu pour vous assurer de ne pas perdre.