Introduction à la gestion des exceptions via HandlerExceptionResolver
Dans les applications web Spring Boot, la gestion globale des exceptions est généralement assurée par l'annotation @ControllerAdvice couplée à @ExceptionHandler. Cependant, le framework Spring MVC offre une alternative moins conventionnelle mais tout à fait fonctionnelle : l'implémentation de l'interface HandlerExceptionResolver. Cet article explore cette approche, son implémentation, et ses particularités, notamment concernant les erreurs HTTP 404.
Configuration du projet
Pour expérimenter cette fonctionnalité, nous devons initialiser une application web Spring Boot standard. Voici une configuration Maven mise à jour et simplifiée :
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Implémentation de HandlerExceptionResolver
1. Création du résolveur personnalisé
L'interface HandlerExceptionResolver ne définit qu'une seule méthode : resolveException. Celle-ci est invoquée lorsqu'nue exception non capturée survient durant le traitement d'une requête. Elle fournit l'objet Exception ainsi que les objets de requête et de réponse.
public class CustomGlobalExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception exception) {
res.setHeader("Content-Type", "application/json; charset=UTF-8");
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
String stackTrace = extractStackTrace(exception);
String jsonResponse = String.format("{\"error\": \"%s\", \"trace\": \"%s\"}",
exception.getMessage(), stackTrace.replace("\"", "\\\""));
try (PrintWriter writer = res.getWriter()) {
writer.write(jsonResponse);
writer.flush();
} catch (IOException ioEx) {
ioEx.printStackTrace();
}
// Retourner null indique que la réponse a été entièrement gérée
return null;
}
private String extractStackTrace(Throwable throwable) {
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString().replace("\n", " | ").replace("\r", "");
}
}
Points clés de cette implémentation :
- Encodage et Type de contenu : Nous définissons explicitement le type de contenu en JSON et l'encodage UTF-8 pour éviter tout problème de caractères spéciaux.
- Écriture directe : Au lieu de retourner un
ModelAndViewpour une vue JSP ou Thymeleaf, nous écrivons directement dans le flux de sortie de la réponse HTTP, ce qui est idéal pour les API REST. - Retour de null : Renvoyer
nullsignale auDispatcherServletque l'exception a été traitée et qu'aucune autre action n'est requise.
2. Enregistrement du résolveur
Pour que Spring MVC prenne en compte notre résolveur, il doit être enregistré dans le contexte de l'application. Cela se fait via l'interface WebMvcConfigurer.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureHandlerExceptionResolvers(List<handlerexceptionresolver> resolvers) {
// Ajout au début de la liste pour prioriser notre résolveur
resolvers.add(0, new CustomGlobalExceptionResolver());
}
}
</handlerexceptionresolver>
Mise en situation et tests
Créons un contrôleur REST simple pour déclencher des erreurs et observer le comportement de notre résolveur.
@RestController
@RequestMapping("/api/math")
public class MathOperationsController {
@GetMapping("/divide")
public int performDivision(@RequestParam int numerator, @RequestParam int denominator) {
return numerator / denominator; // Déclenchera une ArithmeticException si denominator = 0
}
}
Lorsqu'on appelle /api/math/divide?numerator=10&denominator=0, une erreur 500 est générée. Notre CustomGlobalExceptionResolver intercepte l'ArithmeticException et renvoie la trace de la pile au format JSON.
Le cas particulier des erreurs 404 (NoHandlerFoundException)
Si vous testez une URL inexistante (ex: /api/unknown), vous remarquerez que notre résolveur personnalisé n'est pas appelé. Par défaut, Spring Boot délègue la gestion des 404 à une page d'erreur statique ou au BasicErrorController.
Pourquoi ? Lorsqu'une requête ne correspond à aucun @RequestMapping, le DispatcherServlet tente de la résoudre via les gestionnaires de ressources statiques (ResourceHttpRequestHandler). Si aucune ressource n'est trouvée, il ne lève pas immédiatement de NoHandlerFoundException.
Pour forcer la levée de cette exception et la faire passer par notre résolveur, il faut ajuster la configuration dans le fichier application.properties :
# Lève une exception si aucun handler n'est trouvé
spring.mvc.throw-exception-if-no-handler-found=true
# Désactive le mappage des ressources statiques par défaut
spring.web.resources.add-mappings=false
Analyse du DispatcherServlet
Pour comprendre ce mécanisme, il est utile d'observer le code source du DispatcherServlet, spécifiquement la méthode doDispatch :
// Extrait simplifié de la logique interne de Spring
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
// Si aucun handler (y compris statique) n'est trouvé, cette méthode est appelée
noHandlerFound(processedRequest, response);
return;
}
// ...
}
La méthode getHandler itère sur tous les HandlerMapping enregistrés. Si le mappage des ressources statiques est actif, il retournera un handler même pour une URL inconnue, empêchant ainsi l'appel à noHandlerFound.
Comparaison avec @ControllerAdvice
Bien que l'implémentation de HandlerExceptionResolver soit parfaitement fonctionnelle, elle présente certains inconvénients par rapport à l'approche déclarative moderne :
- Verbosité : Manipuler directement les objets
HttpServletRequestetHttpServletResponseainsi que les flux d'entrée/sortie rend le code plus lourd et sujet aux erreurs (comme l'oubli de fermer un flux). - Manque de flexibilité :
@ControllerAdvicepermet de cibler des exceptions spécifiques, de définir facilement les codes HTTP via@ResponseStatus, et de s'intégrer nativement avec les convertisseurs de messages HTTP (comme Jackson pour le JSON) sans avoir à sérialiser manuellement les objets. - Résolveurs par défaut : Spring fournit déjà des implémentations robustes comme
DefaultHandlerExceptionResolverqui traduisent les exceptions Spring en codes HTTP standards.
L'interface HandlerExceptionResolver reste un outil pertinent pour des cas d'usage très spécifiques ou pour des intégrations de bas niveau, tandis que @ControllerAdvice demeure la pratique standard pour la majorité des applications web et API REST.