Comment gérer les exceptions lancées dans les filtres au printemps?


111

Je souhaite utiliser un moyen générique pour gérer les codes d'erreur 5xx, disons spécifiquement le cas où la base de données est en panne dans toute mon application Spring. Je veux un joli json d'erreur au lieu d'une trace de pile.

Pour les contrôleurs, j'ai une @ControllerAdviceclasse pour les différentes exceptions et cela attrape également le cas où la base de données s'arrête au milieu de la demande. Mais ce n'est pas tout. Il se trouve que j'ai aussi une CorsFilterextension personnalisée OncePerRequestFilteret là, lorsque j'appelle, doFilterje reçois le CannotGetJdbcConnectionExceptionet il ne sera pas géré par le @ControllerAdvice. J'ai lu plusieurs choses en ligne qui m'ont rendu encore plus confus.

J'ai donc beaucoup de questions:

  • Dois-je implémenter un filtre personnalisé? J'ai trouvé le ExceptionTranslationFiltermais cela ne gère que AuthenticationExceptionou AccessDeniedException.
  • J'ai pensé à implémenter la mienne HandlerExceptionResolver, mais cela m'a fait douter, je n'ai pas d'exception personnalisée à gérer, il doit y avoir un moyen plus évident que celui-ci. J'ai également essayé d'ajouter un try / catch et d'appeler une implémentation du HandlerExceptionResolver(devrait être assez bon, mon exception n'a rien de spécial) mais cela ne renvoie rien dans la réponse, j'obtiens un statut 200 et un corps vide.

Y a-t-il un bon moyen de gérer cela? Merci


Nous pouvons remplacer BasicErrorController de Spring Boot. J'ai blogué à ce sujet ici: naturalprogrammer.com/blog/1685463/…
Sanjay

Réponses:


87

Voici donc ce que j'ai fait:

J'ai lu les bases sur les filtres ici et j'ai compris que je devais créer un filtre personnalisé qui sera le premier dans la chaîne de filtres et j'aurai un essai pour attraper toutes les exceptions d'exécution qui pourraient s'y produire. Ensuite, je dois créer le json manuellement et le mettre dans la réponse.

Voici donc mon filtre personnalisé:

public class ExceptionHandlerFilter extends OncePerRequestFilter {

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (RuntimeException e) {

            // custom error response class used across my project
            ErrorResponse errorResponse = new ErrorResponse(e);

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.getWriter().write(convertObjectToJson(errorResponse));
    }
}

    public String convertObjectToJson(Object object) throws JsonProcessingException {
        if (object == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(object);
    }
}

Et puis je l'ai ajouté dans le web.xml avant le CorsFilter. Et il fonctionne!

<filter> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <filter-class>xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter</filter-class> 
</filter> 


<filter-mapping> 
    <filter-name>exceptionHandlerFilter</filter-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping> 

<filter> 
    <filter-name>CorsFilter</filter-name> 
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter> 

<filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Pourriez-vous publier votre classe ErrorResponse?
Shiva kumar

1
La classe @Shivakumar ErrorResponse est probablement un DTO simple avec des propriétés de code / message simples.
ratijas

26

Je voulais fournir une solution basée sur la réponse de @kopelitsa . Les principales différences étant:

  1. Réutilisation de la gestion des exceptions du contrôleur à l'aide de HandlerExceptionResolver.
  2. Utilisation de la configuration Java sur la configuration XML

Tout d'abord, vous devez vous assurer que vous disposez d'une classe qui gère les exceptions se produisant dans un RestController / Controller normal (une classe annotée avec @RestControllerAdviceou @ControllerAdviceet une ou plusieurs méthodes annotées avec @ExceptionHandler). Cela gère vos exceptions se produisant dans un contrôleur. Voici un exemple utilisant RestControllerAdvice:

@RestControllerAdvice
public class ExceptionTranslator {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorDTO processRuntimeException(RuntimeException e) {
        return createErrorDTO(HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred.", e);
    }

    private ErrorDTO createErrorDTO(HttpStatus status, String message, Exception e) {
        (...)
    }
}

Pour réutiliser ce comportement dans la chaîne de filtres Spring Security, vous devez définir un filtre et le raccorder à votre configuration de sécurité. Le filtre doit rediriger l'exception vers la gestion des exceptions définie ci-dessus. Voici un exemple:

@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            log.error("Spring Security Filter Chain Exception:", e);
            resolver.resolveException(request, response, null, e);
        }
    }
}

Le filtre créé doit ensuite être ajouté à SecurityConfiguration. Vous devez le raccorder à la chaîne très tôt, car toutes les exceptions du filtre précédent ne seront pas interceptées. Dans mon cas, il était raisonnable de l'ajouter avant le LogoutFilter. Voir la chaîne de filtres par défaut et son ordre dans la documentation officielle . Voici un exemple:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private FilterChainExceptionHandler filterChainExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
            (...)
    }

}

19

Je rencontre ce problème moi-même et j'ai effectué les étapes ci-dessous pour réutiliser mon ExceptionControllerannoté @ControllerAdvisepour le Exceptionslancer dans un filtre enregistré.

Il y a évidemment de nombreuses façons de gérer l'exception mais, dans mon cas, je voulais que l'exception soit gérée par mon ExceptionControllerparce que je suis têtu et aussi parce que je ne veux pas copier / coller le même code (c'est-à-dire que j'ai du traitement / journalisation code dans ExceptionController). Je voudrais retourner la belle JSONréponse, tout comme le reste des exceptions lancées pas d'un filtre.

{
  "status": 400,
  "message": "some exception thrown when executing the request"
}

Quoi qu'il en soit, j'ai réussi à utiliser mon ExceptionHandleret j'ai dû faire un peu plus comme indiqué ci-dessous dans les étapes:

Pas


  1. Vous avez un filtre personnalisé qui peut ou non lever une exception
  2. Vous avez un contrôleur Spring qui gère les exceptions en utilisant @ControllerAdviseie MyExceptionController

Exemple de code

//sample Filter, to be added in web.xml
public MyFilterThatThrowException implements Filter {
   //Spring Controller annotated with @ControllerAdvise which has handlers
   //for exceptions
   private MyExceptionController myExceptionController; 

   @Override
   public void destroy() {
        // TODO Auto-generated method stub
   }

   @Override
   public void init(FilterConfig arg0) throws ServletException {
       //Manually get an instance of MyExceptionController
       ApplicationContext ctx = WebApplicationContextUtils
                  .getRequiredWebApplicationContext(arg0.getServletContext());

       //MyExceptionHanlder is now accessible because I loaded it manually
       this.myExceptionController = ctx.getBean(MyExceptionController.class); 
   }

   @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        try {
           //code that throws exception
        } catch(Exception ex) {
          //MyObject is whatever the output of the below method
          MyObject errorDTO = myExceptionController.handleMyException(req, ex); 

          //set the response object
          res.setStatus(errorDTO .getStatus());
          res.setContentType("application/json");

          //pass down the actual obj that exception handler normally send
          ObjectMapper mapper = new ObjectMapper();
          PrintWriter out = res.getWriter(); 
          out.print(mapper.writeValueAsString(errorDTO ));
          out.flush();

          return; 
        }

        //proceed normally otherwise
        chain.doFilter(request, response); 
     }
}

Et maintenant, l'exemple de Spring Controller qui gère Exceptionles cas normaux (c'est-à-dire les exceptions qui ne sont généralement pas levées au niveau du filtre, celle que nous voulons utiliser pour les exceptions levées dans un filtre)

//sample SpringController 
@ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {

    //sample handler
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(SQLException.class)
    public @ResponseBody MyObject handleSQLException(HttpServletRequest request,
            Exception ex){
        ErrorDTO response = new ErrorDTO (400, "some exception thrown when "
                + "executing the request."); 
        return response;
    }
    //other handlers
}

Partager la solution avec ceux qui souhaitent utiliser ExceptionControllerpour Exceptionsjeté dans un filtre.


10
Eh bien, vous êtes invités à partager votre propre solution qui sonne la façon de le faire :)
Raf

1
Si vous voulez éviter d'avoir un contrôleur câblé dans votre filtre (ce que @ Bato-BairTsyrenov fait référence, je suppose), vous pouvez facilement extraire la logique où vous créez le ErrorDTO dans sa propre @Componentclasse et l'utiliser dans le filtre et dans le controlle.
Rüdiger Schulz

1
Je ne suis pas totalement d'accord avec vous, car ce n'est pas très propre d'injecter un contrôleur spécifique dans votre filtre.
psv

Comme mentionné dans le, answerc'est l'un des moyens! Je n'ai pas prétendu que c'était la meilleure façon. Merci d'avoir partagé votre inquiétude @psv Je suis sûr que la communauté apprécierait la solution que vous avez en tête :)
Raf

12

Alors, voici ce que j'ai fait sur la base d'un amalgame des réponses ci-dessus ... Nous avions déjà un GlobalExceptionHandlerannoté avec @ControllerAdviceet je voulais aussi trouver un moyen de réutiliser ce code pour gérer les exceptions qui proviennent des filtres.

La solution la plus simple que j'ai pu trouver était de laisser le gestionnaire d'exceptions seul et d'implémenter un contrôleur d'erreur comme suit:

@Controller
public class ErrorControllerImpl implements ErrorController {
  @RequestMapping("/error")
  public void handleError(HttpServletRequest request) throws Throwable {
    if (request.getAttribute("javax.servlet.error.exception") != null) {
      throw (Throwable) request.getAttribute("javax.servlet.error.exception");
    }
  }
}

Ainsi, toutes les erreurs causées par des exceptions passent d'abord par le ErrorControlleret sont redirigées vers le gestionnaire d'exceptions en les renvoyant à partir d'un @Controllercontexte, tandis que toutes les autres erreurs (non causées directement par une exception) passent par le ErrorControllersans modification.

Des raisons pour lesquelles c'est en fait une mauvaise idée?


1
Merci de tester cette solution, mais dans mon cas, le travail est parfait.
Maciej

propre et simple un ajout pour spring boot 2.0+ que vous devriez ajouter @Override public String getErrorPath() { return null; }
Fma

vous pouvez utiliser javax.servlet.RequestDispatcher.ERROR_EXCEPTION au lieu de "javax.servlet.error.exception"
Marx

9

Si vous souhaitez une méthode générique, vous pouvez définir une page d'erreur dans web.xml:

<error-page>
  <exception-type>java.lang.Throwable</exception-type>
  <location>/500</location>
</error-page>

Et ajoutez le mappage dans Spring MVC:

@Controller
public class ErrorController {

    @RequestMapping(value="/500")
    public @ResponseBody String handleException(HttpServletRequest req) {
        // you can get the exception thrown
        Throwable t = (Throwable)req.getAttribute("javax.servlet.error.exception");

        // customize response to what you want
        return "Internal server error.";
    }
}

Mais dans une API de repos, la redirection avec l'emplacement n'est pas une bonne solution.
jmattheis

@jmattheis Ce qui précède n'est pas une redirection.
holmis83

Certes, j'ai vu l'emplacement et j'ai pensé qu'il y avait quelque chose à faire avec l'emplacement http. Alors c'est ce dont j'ai besoin (:
jmattheis

Pourriez-vous ajouter la configuration Java équivalente au web.xml, s'il en existe une?
k-den

1
@ k-den Il n'y a pas d'équivalent de configuration Java dans la spécification actuelle, je pense, mais vous pouvez mélanger la configuration web.xml et Java.
holmis83

5

C'est ma solution en remplaçant le gestionnaire Spring Boot / error par défaut

package com.mypackage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * This controller is vital in order to handle exceptions thrown in Filters.
 */
@RestController
@RequestMapping("/error")
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {

    private final static Logger LOGGER = LoggerFactory.getLogger(ErrorController.class);

    private final ErrorAttributes errorAttributes;

    @Autowired
    public ErrorController(ErrorAttributes errorAttributes) {
        Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
        this.errorAttributes = errorAttributes;
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest aRequest, HttpServletResponse response) {
        RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
        Map<String, Object> result =     this.errorAttributes.getErrorAttributes(requestAttributes, false);

        Throwable error = this.errorAttributes.getError(requestAttributes);

        ResponseStatus annotation =     AnnotationUtils.getAnnotation(error.getClass(), ResponseStatus.class);
        HttpStatus statusCode = annotation != null ? annotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;

        result.put("status", statusCode.value());
        result.put("error", statusCode.getReasonPhrase());

        LOGGER.error(result.toString());
        return new ResponseEntity<>(result, statusCode) ;
    }

}

cela affecte-t-il une configuration automatique?
Samet Baskıcı

Notez que HandlerExceptionResolver ne gère pas nécessairement l'exception. Donc, il pourrait tomber en HTTP 200. Utiliser response.setStatus (..) avant de l'appeler semble plus sûr.
ThomasRS

5

Juste pour compléter les autres bonnes réponses fournies, car je voulais trop récemment un seul composant de gestion des erreurs / exceptions dans une simple application SpringBoot contenant des filtres pouvant générer des exceptions, avec d'autres exceptions potentiellement générées par les méthodes du contrôleur.

Heureusement, il semble que rien ne vous empêche de combiner les conseils de votre contrôleur avec un remplacement du gestionnaire d'erreurs par défaut de Spring pour fournir des charges utiles de réponse cohérentes, vous permettre de partager la logique, d'inspecter les exceptions des filtres, d'intercepter les exceptions lancées par un service spécifique, etc.

Par exemple


@ControllerAdvice
@RestController
public class GlobalErrorHandler implements ErrorController {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(ValidationException.class)
  public Error handleValidationException(
      final ValidationException validationException) {
    return new Error("400", "Incorrect params"); // whatever
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler(Exception.class)
  public Error handleUnknownException(final Exception exception) {
    return new Error("500", "Unexpected error processing request");
  }

  @RequestMapping("/error")
  public ResponseEntity handleError(final HttpServletRequest request,
      final HttpServletResponse response) {

    Object exception = request.getAttribute("javax.servlet.error.exception");

    // TODO: Logic to inspect exception thrown from Filters...
    return ResponseEntity.badRequest().body(new Error(/* whatever */));
  }

  @Override
  public String getErrorPath() {
    return "/error";
  }

}

3

Lorsque vous souhaitez tester un état d'application et en cas de problème de retour d'erreur HTTP, je vous suggère un filtre. Le filtre ci-dessous gère toutes les requêtes HTTP. La solution la plus courte dans Spring Boot avec un filtre javax.

Dans la mise en œuvre peut être diverses conditions. Dans mon cas, l'applicationManager teste si l'application est prête.

import ...ApplicationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class SystemIsReadyFilter implements Filter {

    @Autowired
    private ApplicationManager applicationManager;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!applicationManager.isApplicationReady()) {
            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "The service is booting.");
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {}
}

2

Après avoir lu les différentes méthodes suggérées dans les réponses ci-dessus, j'ai décidé de gérer les exceptions d'authentification en utilisant un filtre personnalisé. J'ai pu gérer l'état et les codes de réponse à l'aide d'une classe de réponse d'erreur en utilisant la méthode suivante.

J'ai créé un filtre personnalisé et modifié ma configuration de sécurité en utilisant la méthode addFilterAfter et ajouté après la classe CorsFilter.

@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    //Cast the servlet request and response to HttpServletRequest and HttpServletResponse
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;

    // Grab the exception from the request attribute
    Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
    //Set response content type to application/json
    httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

    //check if exception is not null and determine the instance of the exception to further manipulate the status codes and messages of your exception
    if(exception!=null && exception instanceof AuthorizationParameterNotFoundException){
        ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter writer = httpServletResponse.getWriter();
        writer.write(convertObjectToJson(errorResponse));
        writer.flush();
        return;
    }
    // If exception instance cannot be determined, then throw a nice exception and desired response code.
    else if(exception!=null){
            ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write(convertObjectToJson(errorResponse));
            writer.flush();
            return;
        }
        else {
        // proceed with the initial request if no exception is thrown.
            chain.doFilter(httpServletRequest,httpServletResponse);
        }
    }

public String convertObjectToJson(Object object) throws JsonProcessingException {
    if (object == null) {
        return null;
    }
    ObjectMapper mapper = new ObjectMapper();
    return mapper.writeValueAsString(object);
}
}

Classe SecurityConfig

    @Configuration
    public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    AuthFilter authenticationFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(authenticationFilter, CorsFilter.class).csrf().disable()
                .cors(); //........
        return http;
     }
   }

Classe ErrorResponse

public class ErrorResponse  {
private final String message;
private final String description;

public ErrorResponse(String description, String message) {
    this.message = message;
    this.description = description;
}

public String getMessage() {
    return message;
}

public String getDescription() {
    return description;
}}

0

Vous pouvez utiliser la méthode suivante dans le bloc catch:

response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")

Notez que vous pouvez utiliser n'importe quel code HttpStatus et un message personnalisé.


-1

C'est étrange parce que @ControllerAdvice devrait fonctionner, attrapez-vous la bonne exception?

@ControllerAdvice
public class GlobalDefaultExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = DataAccessException.class)
    public String defaultErrorHandler(HttpServletResponse response, DataAccessException e) throws Exception {
       response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
       //Json return
    }
}

Essayez également d'attraper cette exception dans CorsFilter et envoyez une erreur 500, quelque chose comme ça

@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    //Json return
}

La gestion de l'exception dans CorsFilter fonctionne, mais ce n'est pas très propre. En fait, ce dont j'ai vraiment besoin, c'est de gérer l'exception pour tous les filtres
kopelitsa

35
Le lancer d'exception de Filterpeut ne pas être rattrapé @ControllerAdvicecar in peut ne pas atteindre DispatcherServlet.
Thanh Nguyen Van

-1

Vous n'avez pas besoin de créer un filtre personnalisé pour cela. Nous avons résolu ce problème en créant des exceptions personnalisées qui étendent ServletException (qui est levée à partir de la méthode doFilter, indiquée dans la déclaration). Ceux-ci sont ensuite capturés et gérés par notre gestionnaire d'erreurs global.

modifier: grammaire


Pourriez-vous partager un extrait de code de votre gestionnaire d'erreurs global?
Neeraj Vernekar le

ça ne marche pas pour moi. J'ai créé une exception personnalisée, qui étend ServletException, ajouté la prise en charge de cette exception dans ExceptionHandler, mais elle n'a pas été interceptée ici.
Marx
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.