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

Quiero usar una forma genérica para administrar los códigos de error 5xx, digamos específicamente el caso cuando la base de datos está bajando en toda mi aplicación de spring. Quiero un bonito error json en lugar de un rastro de stack.

Para los controladores, tengo una clase @ControllerAdvice para las diferentes excepciones, y esto también capta el caso de que el db se detenga en el medio de la solicitud. Pero esto no es todo. También tengo un CorsFilter personalizado que extiende OncePerRequestFilter y allí, cuando llamo a doFilter obtengo la CannotGetJdbcConnectionException y no la administro @ControllerAdvice . Leí varias cosas en línea que solo me confundieron más.

Entonces tengo muchas preguntas:

  • ¿Debo implementar un filtro personalizado? Encontré el ExceptionTranslationFilter pero esto solo maneja AuthenticationException o AccessDeniedException .
  • Pensé en implementar mi propio HandlerExceptionResolver , pero esto me hizo dudar, no tengo ninguna excepción personalizada para administrar, debe haber una manera más obvia que esta. También traté de 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 manera de lidiar con esto? Gracias

Entonces esto es lo que hice:

Leí los conceptos básicos sobre los filtros aquí y me di cuenta de que necesito crear un filtro personalizado que será el primero en la cadena de filtros y tendrá un try catch para atrapar 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 el web.xml antes del CorsFilter . ¡Y funciona!

  exceptionHandlerFilter xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter   exceptionHandlerFilter /*   CorsFilter org.springframework.web.filter.DelegatingFilterProxy   CorsFilter /*  

Me encontré con este problema yo mismo y realicé los pasos a continuación para reutilizar mi ExceptionController que está anotado con @ControllerAdvise para Exceptions lanzadas en un filtro registrado.

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

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

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

Pasos


  1. Tienes un filtro personalizado que puede lanzar o no una excepción
  2. Usted tiene un controlador de Spring que maneja excepciones usando @ControllerAdvise ie 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 Spring Controller de ejemplo que maneja Exception en 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 } 

Compartir la solución con aquellos que deseen usar ExceptionController para Exceptions lanzadas en un filtro.

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

  java.lang.Throwable /500  

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

Esta es mi solución al anular el gestor de arranque / error Spring Spring 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> error(HttpServletRequest aRequest, HttpServletResponse response) { RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest); Map 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) ; } } 

Cuando quiera probar un estado de la aplicación y en caso de que haya un problema, devuelva un error de HTTP. Sugeriría un filtro. El filtro a continuación 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() {} } 

Es extraño porque @ControllerAdvice debería funcionar, ¿está captando 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 trate de detectar esta excepción en CorsFilter y envíe 500 error, algo como esto

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

Entonces, esto es lo que hice basado en una amalgama de las respuestas anteriores … Ya teníamos un GlobalExceptionHandler anotado con @ControllerAdvice y también quería encontrar una manera de volver a usar ese código para manejar las excepciones que provienen de los filtros.

La solución más simple que pude encontrar fue dejar solo el controlador de excepciones 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"); } } } 

Entonces, cualquier error causado por excepciones pasa primero a través del ErrorController y se ErrorController al manejador de excepciones volviéndolo a lanzar dentro de un contexto @Controller , mientras que cualquier otro error (no causado directamente por una excepción) pasa a través del ErrorController sin modificación .

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