Traitement des erreurs pour REST avec Spring

Traitement des erreurs pour REST avec Spring

1. Vue d'ensemble

Cet article illustrerahow to implement Exception Handling with Spring for a REST API. Nous allons également avoir un aperçu de l'historique et voir quelles nouvelles options les différentes versions ont introduites.

Before Spring 3.2, the two main approaches to handling exceptions in a Spring MVC application were: HandlerExceptionResolver or the @ExceptionHandler annotation. Les deux ont des inconvénients évidents.

Since 3.2 we’ve had the @ControllerAdvice annotation pour répondre aux limitations des deux solutions précédentes et pour promouvoir une gestion unifiée des exceptions dans toute une application.

Maintenant,Spring 5 introduces the ResponseStatusException class: un moyen rapide de gérer les erreurs de base dans nos API REST.

Tous ces éléments ont une chose en commun: ils gèrent très bien lesseparation of concerns. L'application peut générer une exception normalement pour indiquer une défaillance quelconque - des exceptions qui seront ensuite traitées séparément.

Enfin, nous verrons ce que Spring Boot apporte à la table et comment nous pouvons le configurer en fonction de nos besoins.

Lectures complémentaires:

Gestion personnalisée des messages d'erreur pour l'API REST

Implémentez un gestionnaire d'exception global pour une API REST avec Spring.

Read more

Guide des validateurs REST de données de printemps

Guide rapide et pratique sur les validateurs Spring Data REST

Read more

Spring MVC Custom Validation

Apprenez à créer une annotation de validation personnalisée et à l'utiliser dans Spring MVC.

Read more

2. Solution 1 - Le niveau du contrôleur@ExceptionHandler

La première solution fonctionne au niveau@Controller - nous allons définir une méthode pour gérer les exceptions et l'annoter avec@ExceptionHandler:

public class FooController{

    //...
    @ExceptionHandler({ CustomException1.class, CustomException2.class })
    public void handleException() {
        //
    }
}

Cette approche présente un inconvénient majeur -the @ExceptionHandler annotated method is only active for that particular Controller, pas globalement pour l'ensemble de l'application. Bien sûr, l'ajout à chaque contrôleur le rend mal adapté à un mécanisme général de gestion des exceptions.

Nous pouvons contourner cette limitation en ayantall Controllers extend a Base Controller class - cependant, cela peut être un problème pour les applications où, pour une raison quelconque, ce n'est pas possible. Par exemple, les contrôleurs peuvent déjà s’étendre à partir d’une autre classe de base pouvant figurer dans un autre conteneur ou ne pas être directement modifiable, ou ne pas être eux-mêmes directement modifiable.

Ensuite, nous examinerons une autre façon de résoudre le problème de gestion des exceptions - une méthode qui est globale et n'inclut aucune modification des artefacts existants tels que les contrôleurs.

3. Solution 2 -The HandlerExceptionResolver

La deuxième solution consiste à définir unHandlerExceptionResolver - cela résoudra toute exception levée par l'application. Cela nous permettra également d'implémenter ununiform exception handling mechanism dans notre API REST.

Avant d'opter pour un résolveur personnalisé, passons en revue les implémentations existantes.

3.1. ExceptionHandlerExceptionResolver

Ce résolveur a été introduit dans Spring 3.1 et est activé par défaut dans lesDispatcherServlet. C'est en fait le composant central du fonctionnement du mécanisme @ExceptionHandler présenté précédemment.

3.2. DefaultHandlerExceptionResolver

Ce résolveur a été introduit dans Spring 3.0, et il est activé par défaut dans lesDispatcherServlet. Il est utilisé pour résoudre les exceptions Spring standard à leurs codes d'état HTTP correspondants, à savoir Erreur client -4xx et Erreur serveur - codes d'état5xx. Here’s the full list des exceptions Spring qu'il gère, et comment ils correspondent aux codes d'état.

Bien qu'il définisse correctement le code d'état de la réponse, unlimitation is that it doesn’t set anything to the body of the Response. Et pour une API REST - le code d’état n’est vraiment pas assez d’informations à présenter au client - la réponse doit également avoir un corps pour permettre à l’application de donner des informations supplémentaires sur l’échec.

Cela peut être résolu en configurant la résolution de la vue et en rendant le contenu d'erreur viaModelAndView, mais la solution n'est clairement pas optimale. C’est pourquoi Spring 3.2 a introduit une meilleure option dont nous parlerons dans une section ultérieure.

3.3. ResponseStatusExceptionResolver

Ce résolveur a également été introduit dans Spring 3.0 et est activé par défaut dans lesDispatcherServlet. Sa principale responsabilité est d'utiliser l'annotation@ResponseStatus disponible sur les exceptions personnalisées et de mapper ces exceptions aux codes d'état HTTP.

Une telle exception personnalisée peut ressembler à:

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    public MyResourceNotFoundException() {
        super();
    }
    public MyResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyResourceNotFoundException(String message) {
        super(message);
    }
    public MyResourceNotFoundException(Throwable cause) {
        super(cause);
    }
}

Identique auDefaultHandlerExceptionResolver, ce résolveur est limité dans la façon dont il traite le corps de la réponse - il mappe le code d'état sur la réponse, mais le corps est toujoursnull.

3.4. SimpleMappingExceptionResolver et AnnotationMethodHandlerExceptionResolver

LeSimpleMappingExceptionResolver existe depuis un certain temps - il vient de l'ancien modèle Spring MVC et estnot very relevant for a REST Service. Nous l'utilisons essentiellement pour mapper les noms de classe d'exceptions afin d'afficher les noms.

LeAnnotationMethodHandlerExceptionResolver a été introduit dans Spring 3.0 pour gérer les exceptions via l'annotation@ExceptionHandler mais a été déconseillé parExceptionHandlerExceptionResolver à partir de Spring 3.2.

3.5. PersonnaliséHandlerExceptionResolver

La combinaison deDefaultHandlerExceptionResolver etResponseStatusExceptionResolver contribue grandement à fournir un bon mécanisme de gestion des erreurs pour un service Spring RESTful. L'inconvénient est - comme mentionné précédemment -no control over the body of the response.

Dans l'idéal, nous aimerions pouvoir générer soit JSON soit XML, en fonction du format demandé par le client (via l'en-têteAccept).

Cela justifie à lui seul la création dea new, custom exception resolver:

@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    protected ModelAndView doResolveException(
      HttpServletRequest request,
      HttpServletResponse response,
      Object handler,
      Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                return handleIllegalArgument((IllegalArgumentException) ex, response, handler);
            }
            ...
        } catch (Exception handlerException) {
            logger.warn("Handling of [" + ex.getClass().getName() + "]
              resulted in Exception", handlerException);
        }
        return null;
    }

    private ModelAndView
      handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response)
      throws IOException {
        response.sendError(HttpServletResponse.SC_CONFLICT);
        String accept = request.getHeader(HttpHeaders.ACCEPT);
        ...
        return new ModelAndView();
    }
}

Un détail à noter ici est que nous avons accès aurequest lui-même, donc nous pouvons considérer la valeur de l'en-têteAccept envoyé par le client.

Par exemple, si le client demandeapplication/json alors, dans le cas d'une condition d'erreur, nous voulons nous assurer que nous retournons un corps de réponse encodé avecapplication/json.

L'autre détail important de l'implémentation est quewe return a ModelAndView – this is the body of the response et cela nous permettra de définir tout ce qui est nécessaire dessus.

Cette approche est un mécanisme cohérent et facilement configurable pour le traitement des erreurs d’un service REST Spring. Il a cependant des limites: il interagit avec lesHtttpServletResponse de bas niveau et s'intègre dans l'ancien modèle MVC qui utiliseModelAndView - il y a donc encore place à l'amélioration.

4. Solution 3 -@ControllerAdvice

Spring 3.2 prend en charge lesa global @ExceptionHandler with the @ControllerAdvice annotation. Cela permet un mécanisme qui rompt avec l'ancien modèle MVC et utiliseResponseEntity avec la sécurité de type et la flexibilité de@ExceptionHandler:

@ControllerAdvice
public class RestResponseEntityExceptionHandler
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value
      = { IllegalArgumentException.class, IllegalStateException.class })
    protected ResponseEntity handleConflict(
      RuntimeException ex, WebRequest request) {
        String bodyOfResponse = "This should be application specific";
        return handleExceptionInternal(ex, bodyOfResponse,
          new HttpHeaders(), HttpStatus.CONFLICT, request);
    }
}


L'annotation@ControllerAdvice nous permet deconsolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

Le mécanisme actuel est extrêmement simple mais aussi très flexible. Cela nous donne:

  • Contrôle total sur le corps de la réponse ainsi que sur le code d'état

  • Cartographie de plusieurs exceptions à la même méthode, à traiter ensemble, et

  • Il fait bon usage de la nouvelle réponse RESTfulResposeEntity __

Une chose à garder à l'esprit ici est dematch the exceptions declared with @ExceptionHandler with the exception used as the argument of the method. Si ceux-ci ne correspondent pas, le compilateur ne se plaindra pas - aucune raison qu'il le devrait, et Spring ne se plaindra pas non plus.

Cependant, lorsque l'exception est réellement levée lors de l'exécution,the exception resolving mechanism will fail with:

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5. Solution 4 -ResponseStatusException (printemps 5 et plus)

Spring 5 a introduit la classeResponseStatusException. Nous pouvons en créer une instance fournissant unHttpStatus et éventuellement unreason et uncause:

@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
    try {
        Foo resourceById = RestPreconditions.checkFound(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
        return resourceById;
     }
    catch (MyResourceNotFoundException exc) {
         throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Foo Not Found", exc);
    }
}

Quels sont les avantages de l'utilisation deResponseStatusException?

  • Excellent pour le prototypage: nous pouvons mettre en œuvre une solution de base assez rapidement

  • Un type, plusieurs codes d'état: Un type d'exception peut entraîner plusieurs réponses différentes. This reduces tight coupling compared to the @ExceptionHandler

  • Nous n'aurons pas à créer autant de classes d'exceptions personnalisées

  • More control over exception handling car les exceptions peuvent être créées par programme

Et qu'en est-il des compromis?

  • Il n’existe pas de méthode unifiée de gestion des exceptions: il est plus difficile d’appliquer certaines conventions à l’échelle de l’application, contrairement à@ControllerAdvice qui offre une approche globale

  • Duplication de code: il est possible que nous reproduisions du code dans plusieurs contrôleurs.

Il convient également de noter qu’il est possible de combiner différentes approches au sein d’une même application.

For example, we can implement a @ControllerAdvice globally, but also ResponseStatusExceptions locally. Cependant, nous devons être prudents: si la même exception peut être gérée de plusieurs manières, nous pouvons remarquer un comportement surprenant. Une convention possible consiste à traiter un type d’exception spécifique toujours d’une manière.

Pour plus de détails et d'autres exemples, consultez nostutorial on ResponseStatusException.

6. Gérer l'accès refusé dans Spring Security

L’accès refusé se produit lorsqu'un utilisateur authentifié tente d’accéder à des ressources auxquelles il n’a pas suffisamment d’autorités pour accéder.

6.1. MVC - Page d'erreur personnalisée

Tout d'abord, examinons le style MVC de la solution et voyons comment personnaliser une page d'erreur pour l'accès refusé:

La configuration XML:


    
    ...
    

Et la configuration Java:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedPage("/my-error-page");
}

Lorsque les utilisateurs essaient d'accéder à une ressource sans avoir suffisamment d'autorités, ils seront redirigés vers «/my-error-page».

6.2. PersonnaliséAccessDeniedHandler

Voyons ensuite comment écrire nosAccessDeniedHandler personnalisés:

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle
      (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
      throws IOException, ServletException {
        response.sendRedirect("/my-error-page");
    }
}

Et maintenant, configurons-le en utilisantXML Configuration:


    
    ...
    

Ou en utilisant la configuration Java:

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
}

Notez comment - dans nosCustomAccessDeniedHandler, nous pouvons personnaliser la réponse à notre guise en redirigeant ou en affichant un message d'erreur personnalisé.

6.3. Sécurité au niveau REST et méthode

Enfin, voyons comment gérer la sécurité au niveau de la méthode@PreAuthorize,@PostAuthorize et@Secure Accès refusé.

Nous allons, bien sûr, utiliser le mécanisme de gestion globale des exceptions dont nous avons parlé précédemment pour gérer également lesAccessDeniedException:

@ControllerAdvice
public class RestResponseEntityExceptionHandler
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ AccessDeniedException.class })
    public ResponseEntity handleAccessDeniedException(
      Exception ex, WebRequest request) {
        return new ResponseEntity(
          "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
    }

    ...
}





7. Prise en charge du démarrage du printemps

Spring Boot fournit une implémentation deErrorController pour gérer les erreurs de manière raisonnable.

En bref, il sert une page d'erreur de secours pour les navigateurs (également appelée page d'erreur Whitelabel) et une réponse JSON pour les requêtes RESTful, non HTML:

{
    "timestamp": "2019-01-17T16:12:45.977+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Error processing the request!",
    "path": "/my-endpoint-with-exceptions"
}

Comme d’habitude, Spring Boot permet de configurer ces fonctionnalités avec les propriétés suivantes:

  • server.error.whitelabel.enabled: peut être utilisé pour désactiver la page d'erreur Whitelabel et s'appuyer sur le conteneur de servlet pour fournir un message d'erreur HTML

  • server.error.include-stacktrace: with une valeuralways , il inclut le stacktrace à la fois dans la réponse par défaut HTML et JSON

En dehors de ces propriétés,we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

Nous pouvons également personnaliser les attributs que nous voulons afficher dans la réponse en incluant unErrorAttributes bean dans le contexte. Nous pouvons étendre la classeDefaultErrorAttributes fournie par Spring Boot pour faciliter les choses:

@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map getErrorAttributes(
      WebRequest webRequest, boolean includeStackTrace) {
        Map errorAttributes =
          super.getErrorAttributes(webRequest, includeStackTrace);
        errorAttributes.put("locale", webRequest.getLocale()
            .toString());
        errorAttributes.remove("error");

        //...

        return errorAttributes;
    }
}

Si nous voulons aller plus loin et définir (ou remplacer) comment l'application gérera les erreurs pour un type de contenu particulier, nous pouvons enregistrer un sbeanErrorController .

Encore une fois, nous pouvons utiliser leBasicErrorController par défaut fourni par Spring Boot pour nous aider.

Par exemple, imaginons que nous voulions personnaliser la manière dont notre application traite les erreurs déclenchées dans les points de terminaison XML. Tout ce que nous avons à faire est de définir une méthode publique en utilisant les@RequestMapping et en indiquant qu'elle produit le type de médiaapplication/xml:

@Component
public class MyErrorController extends BasicErrorController {

    public MyErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity> xmlError(HttpServletRequest request) {

    // ...

    }
}

8. Conclusion

Ce didacticiel a présenté plusieurs méthodes pour implémenter un mécanisme de gestion des exceptions pour une API REST dans Spring, en commençant par le mécanisme plus ancien et en poursuivant avec la prise en charge de Spring 3.2, puis dans les versions 4.x et 5.x.

Comme toujours, le code présenté dans cet article est disponibleover on Github.

Pour le code lié à Spring Security, vous pouvez vérifier le modulespring-security-rest.