¿Cómo administrar las excepciones lanzadas en los filtros en Spring?


106

Quiero usar una forma genérica de administrar códigos de error 5xx, digamos específicamente el caso cuando la base de datos está inactiva en toda mi aplicación de primavera. Quiero un bonito json de error en lugar de un seguimiento de pila.

Para los controladores, tengo una @ControllerAdviceclase para las diferentes excepciones y esto también está detectando el caso de que la base de datos se detiene en medio de la solicitud. Pero esto no es todo. También tengo una CorsFilterextensión personalizada OncePerRequestFiltery allí, cuando llamo, doFilterobtengo el CannotGetJdbcConnectionExceptiony no será administrado por el @ControllerAdvice. Leí varias cosas en línea que solo me confundieron más.

Entonces tengo muchas preguntas:

  • ¿Necesito implementar un filtro personalizado? Encontré el ExceptionTranslationFilterpero esto solo maneja AuthenticationExceptiono AccessDeniedException.
  • Pensé en implementar la mía propia HandlerExceptionResolver, pero esto me hizo dudar, no tengo ninguna excepción personalizada para administrar, debe haber una forma más obvia que esta. También intenté agregar un try / catch y llamar a una implementación de HandlerExceptionResolver(debería ser lo suficientemente bueno, mi excepción no es nada especial) pero esto no devuelve nada en la respuesta, obtengo un estado 200 y un cuerpo vacío.

¿Hay alguna buena forma de lidiar con esto? Gracias


Podemos anular el BasicErrorController de Spring Boot. He escrito un blog sobre esto aquí: naturalprogrammer.com/blog/1685463/…
Sanjay

Respuestas:


83

Entonces esto es lo que hice:

Leí los conceptos básicos sobre los filtros aquí y descubrí que necesito crear un filtro personalizado que será el primero en la cadena de filtros y tendrá una captura de prueba para detectar todas las excepciones de tiempo de ejecución que puedan ocurrir allí. Entonces necesito crear el json manualmente y ponerlo en la respuesta.

Así que aquí está mi filtro personalizado:

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);
    }
}

Y luego lo agregué en web.xml antes de CorsFilter. ¡Y funciona!

<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>

¿Podrías publicar tu clase ErrorResponse?
Shiva kumar

La clase @Shivakumar ErrorResponse es probablemente un DTO simple con propiedades simples de código / mensaje.
ratijas

20

Quería brindar una solución basada en la respuesta de @kopelitsa . Las principales diferencias son:

  1. Reutilizando el manejo de excepciones del controlador usando el HandlerExceptionResolver.
  2. Usando la configuración de Java sobre la configuración XML

Primero, debe asegurarse de tener una clase que maneja las excepciones que ocurren en un RestController / Controller normal (una clase anotada con @RestControllerAdviceo @ControllerAdvicey métodos anotados con @ExceptionHandler). Esto maneja sus excepciones que ocurren en un controlador. Aquí hay un ejemplo usando 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) {
        (...)
    }
}

Para reutilizar este comportamiento en la cadena de filtros de Spring Security, debe definir un Filtro y conectarlo a su configuración de seguridad. El filtro necesita redirigir la excepción al manejo de excepciones definido anteriormente. Aquí hay un ejemplo:

@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);
        }
    }
}

Luego, el filtro creado debe agregarse a SecurityConfiguration. Debe conectarlo a la cadena muy pronto, porque no se detectarán todas las excepciones de filtro anteriores. En mi caso, era razonable agregarlo antes del LogoutFilter. Vea la cadena de filtros predeterminada y su orden en los documentos oficiales . Aquí hay un ejemplo:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private FilterChainExceptionHandler filterChainExceptionHandler;

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

}

17

Me encontré con este problema y realicé los pasos a continuación para reutilizar mi ExceptionControllerque está anotado @ControllerAdvisepara Exceptionsarrojarlo en un filtro registrado.

Obviamente, hay muchas formas de manejar la excepción pero, en mi caso, quería que la excepción fuera manejada por mi ExceptionControllerporque soy terco y también porque no quiero copiar / pegar el mismo código (es decir, tengo algo de procesamiento / registro código en ExceptionController). Me gustaría devolver la hermosa JSONrespuesta al igual que el resto de las excepciones lanzadas no desde un filtro.

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

De todos modos, logré hacer uso de mi ExceptionHandlery tuve que hacer un poco más como se muestra a continuación en los pasos:

Pasos


  1. Tiene un filtro personalizado que puede o no generar una excepción
  2. Tiene un controlador Spring que maneja excepciones usando, por @ControllerAdviseejemplo, MyExceptionController

Código de muestra

//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); 
     }
}

Y ahora el controlador Spring de muestra que se maneja Exceptionen casos normales (es decir, excepciones que generalmente no se lanzan en el nivel de filtro, el que queremos usar para las excepciones lanzadas en un filtro)

//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
}

Compartiendo la solución con quienes deseen utilizarla ExceptionControllerpara Exceptionslanzarla en un Filtro.


10
Bueno, puede compartir su propia solución que suena como la manera de hacerlo :)
Raf

1
Si desea evitar tener un controlador conectado a su filtro (que es a lo que se refiere @ Bato-BairTsyrenov, supongo), puede extraer fácilmente la lógica donde crea el ErrorDTO en su propia @Componentclase y usarla en el Filtro y en el controlador.
Rüdiger Schulz

1
No estoy totalmente de acuerdo contigo, porque esto no es muy limpio para inyectar un controlador específico en tu filtro.
psv

Como se menciona en el, answeresta es una de las formas. No he dicho que sea la mejor forma. Gracias por compartir su inquietud @psv Estoy seguro de que la comunidad apreciará la solución que tiene en mente :)
Raf

12

Entonces, esto es lo que hice basado en una combinación de las respuestas anteriores ... Ya teníamos una GlobalExceptionHandleranotación con @ControllerAdvicey también quería encontrar una manera de reutilizar ese código para manejar las excepciones que provienen de los filtros.

La solución más simple que pude encontrar fue dejar el controlador de excepciones solo e implementar un controlador de errores de la siguiente manera:

@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");
    }
  }
}

Por lo tanto, los errores causados ​​por excepciones primero pasan por el ErrorControllery se redirigen al controlador de excepciones volviéndolos a lanzar desde dentro de un @Controllercontexto, mientras que cualquier otro error (no causado directamente por una excepción) pasa por el ErrorControllersin modificación.

¿Alguna razón por la que esto es realmente una mala idea?


1
Gracias ahora probando esta solución pero en mi caso funciona perfecto.
Maciej

una adición limpia y simple para Spring Boot 2.0+ que debe agregar @Override public String getErrorPath() { return null; }
Fma

puede usar javax.servlet.RequestDispatcher.ERROR_EXCEPTION en lugar de "javax.servlet.error.exception"
Marx

9

Si desea una forma genérica, puede definir una página de error en web.xml:

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

Y agregue mapeo en 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.";
    }
}

Pero en un descanso, la redirección de API con la ubicación no es una buena solución.
jmattheis

@jmattheis Lo anterior no es una redirección.
holmis83

Es cierto, vi la ubicación y pensé que tenía algo que ver con la ubicación http. Entonces esto es lo que necesito (:
jmattheis

¿Podría agregar la configuración de Java equivalente a web.xml, si existe?
k-den

1
@ k-den Creo que no hay un equivalente de configuración de Java en la especificación actual, pero puede mezclar web.xml y configuración de Java.
holmis83

5

Esta es mi solución al anular el controlador Spring Boot / error predeterminado

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) ;
    }

}

¿Afecta alguna configuración automática?
Samet Baskıcı

Tenga en cuenta que HandlerExceptionResolver no maneja necesariamente la excepción. Por lo que podría caer como HTTP 200. Usar response.setStatus (..) antes de llamarlo parece más seguro.
ThomasRS

5

Solo para complementar las otras buenas respuestas proporcionadas, ya que recientemente quería un solo componente de manejo de errores / excepciones en una aplicación SpringBoot simple que contiene filtros que pueden arrojar excepciones, con otras excepciones potencialmente lanzadas desde métodos de controlador.

Afortunadamente, parece que no hay nada que le impida combinar los consejos de su controlador con una anulación del controlador de errores predeterminado de Spring para proporcionar cargas útiles de respuesta coherentes, permitirle compartir lógica, inspeccionar excepciones de filtros, capturar excepciones específicas lanzadas por servicios, etc.

P.ej


@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

Cuando desee probar el estado de una aplicación y, en caso de un problema, devolver el error HTTP, sugeriría un filtro. El siguiente filtro maneja todas las solicitudes HTTP. La solución más corta en Spring Boot con un filtro javax.

En la implementación puede haber varias condiciones. En mi caso, el applicationManager prueba si la aplicación está lista.

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

Después de leer los diferentes métodos sugeridos en las respuestas anteriores, decidí manejar las excepciones de autenticación utilizando un filtro personalizado. Pude manejar el estado y los códigos de respuesta usando una clase de respuesta de error usando el siguiente método.

Creé un filtro personalizado y modifiqué mi configuración de seguridad usando el método addFilterAfter y lo agregué después de la clase 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);
}
}

Clase 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;
     }
   }

Clase 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

Puede utilizar el siguiente método dentro del bloque de captura:

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

Tenga en cuenta que puede usar cualquier código HttpStatus y un mensaje personalizado.


-1

Es extraño porque @ControllerAdvice debería funcionar, ¿está detectando la excepción correcta?

@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
    }
}

También intente capturar esta excepción en CorsFilter y envíe el error 500, algo como esto

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

Manejar la excepción en CorsFilter funciona, pero no es muy limpio. De hecho, lo que realmente necesito es manejar la excepción para todos los filtros
kopelitsa

35
Es posible que el lanzamiento de excepción Filterno se atrape @ControllerAdviceporque es posible que no alcance DispatcherServlet.
Thanh Nguyen Van

-1

No es necesario crear un filtro personalizado para esto. Resolvimos esto creando excepciones personalizadas que extienden ServletException (que se lanza desde el método doFilter, que se muestra en la declaración). Estos son luego capturados y manejados por nuestro controlador de errores global.

editar: gramática


¿Le importaría compartir un fragmento de código de su controlador de errores global?
Neeraj Vernekar

no me funciona. Hice una excepción personalizada, que extiende ServletException, agregué soporte para esta excepción en ExceptionHandler, pero no ha sido interceptada allí.
Marx
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.