web-dev-qa-db-fra.com

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

Je veux 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 sur l'ensemble de mon application Spring. Je veux une jolie erreur json au lieu d'une trace de pile. 

Pour les contrôleurs, j'ai une classe @ControllerAdvice pour les différentes exceptions, ce qui attrape également le cas où la base de données est en train de s'arrêter au milieu de la demande. Mais ce n'est pas tout. Il se trouve que j’ai aussi une variable CorsFilter étendue OncePerRequestFilter et lorsque j’appelle doFilter j’obtiens la CannotGetJdbcConnectionException et elle ne sera pas gérée par le @ControllerAdvice. J'ai lu plusieurs choses en ligne qui ne faisaient que me rendre plus confus. 

J'ai donc beaucoup de questions:

  • Dois-je implémenter un filtre personnalisé? J'ai trouvé ExceptionTranslationFilter mais cela ne traite que AuthenticationException ou AccessDeniedException
  • J'ai pensé implémenter ma propre HandlerExceptionResolver, mais cela m'a fait douter, je n'ai aucune exception personnalisée à gérer, il doit y avoir un moyen plus évident que celui-là. J'ai aussi essayé d'ajouter un try/catch and call une implémentation de la HandlerExceptionResolver (ça devrait être suffisant, mon exception n'a rien de spécial) mais cela ne renvoie rien dans la réponse, je reçois un statut 200 et un corps vide. 

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

51
kopelitsa

Alors voici ce que j'ai fait:

J'ai lu les notions de base sur les filtres ici et je me suis rendu compte que je devais créer un filtre personnalisé qui figurerait en premier dans la chaîne de filtres. Ensuite, je dois créer le JSON manuellement et le mettre dans la réponse.

Alors voici 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 fichier web.xml avant la 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>
49
kopelitsa

Je rencontre ce problème moi-même et j'ai suivi les étapes ci-dessous pour réutiliser ma ExceptionController annotée avec @ControllerAdvise pour Exceptions envoyée dans un filtre enregistré.

Il y a évidemment beaucoup de façons de gérer l'exception mais, dans mon cas, je voulais que l'exception soit gérée par ma ExceptionController parce que je suis têtue et aussi parce que je ne veux pas copier/coller le même code code de connexion dans ExceptionController). Je voudrais renvoyer la belle réponse JSON de la même manière que le reste des exceptions rejeté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 ma ExceptionHandler et j’ai dû faire un petit extra comme indiqué ci-dessous par é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 à l'aide de @ControllerAdvise, par exemple, 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 contrôleur de ressort qui gère Exception dans des cas normaux (c'est-à-dire les exceptions qui ne sont généralement pas générées dans le niveau de filtre, celle que nous souhaitons utiliser pour les exceptions généré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
}

Partage de la solution avec ceux qui souhaitent utiliser ExceptionController pour Exceptions jeté dans un filtre.

11
Raf

Si vous voulez un moyen 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 un 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.";
    }
}
8
holmis83

Ceci est ma solution en surchargeant le gestionnaire d'erreurs Spring Boot 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) ;
    }

}
5
walv

Donc, voici ce que j'ai fait en fusionnant les réponses ci-dessus ... Nous avions déjà une variable GlobalExceptionHandler annotée avec @ControllerAdvice et je voulais aussi trouver un moyen de réutiliser ce code pour gérer les exceptions issues des filtres.

La solution la plus simple que j'ai trouvée consistait à laisser le gestionnaire d'exceptions seul et à 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 traversent d'abord la variable ErrorController et sont redirigées vers le gestionnaire d'exceptions en les renvoyant à l'intérieur d'un contexte @Controller, tandis que toutes les autres erreurs (non directement causées par une exception) traversent la variable ErrorController sans modification.

Des raisons pour lesquelles c'est réellement une mauvaise idée?

5
AndyB

Lorsque vous souhaitez tester un état d'application et en cas de problème, renvoyer une erreur HTTP, je 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() {}
}
4
Cyva

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 la variable HandlerExceptionResolver.
  2. Utilisation de la configuration Java sur la configuration XML

Tout d’abord, vous devez vous assurer que vous avez une classe qui gère les exceptions survenant dans un RestController/Controller normal (une classe annotée avec @RestControllerAdvice ou @ControllerAdvice et une ou plusieurs méthodes annotées avec @ExceptionHandler). Ceci gère vos exceptions survenant 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 connecter à 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 (RuntimeException e) {
            log.error("Spring Security Filter Chain RuntimeException:", e);
            resolver.resolveException(request, response, null, e);
        }
    }
}

Le filtre créé doit ensuite être ajouté à SecurityConfiguration. Vous devez le connecter très tôt à la chaîne, 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)
            (...)
    }

}
1
ssc-hrep3

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 d’envoyer une erreur 500, quelque chose comme ceci.

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

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

J'ai créé un filtre personnalisé et modifié ma configuration de sécurité à l'aide de la méthode addFilterAfter, puis 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
Adewagold

Juste pour compléter les autres réponses fournies, car je souhaitais trop récemment un composant de traitement single error/exception 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, rien ne vous empêche apparemment de combiner les conseils du contrôleur avec une substitution du gestionnaire d’erreurs par défaut de Spring afin de fournir des charges de réponse cohérentes, de partager la logique, de contrôler les exceptions à partir de filtres, de dérouter des exceptions spécifiques générées par le service, 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";
  }

}
0
Tom Bunting