Servicio de excepción de Spring Boot REST handling

Estoy tratando de configurar un servidor de servicios REST a gran escala. Estamos usando Spring Boot 1.2.1 Spring 4.1.5 y Java 8. Nuestros controladores están implementando @RestController y las anotaciones @RequestMapping estándar.

Mi problema es que Spring Boot configura un redireccionamiento predeterminado para excepciones de controlador /error . De los documentos:

Spring Boot proporciona un mapeo / error de forma predeterminada que maneja todos los errores de una manera sensata, y se registra como una página de error “global” en el contenedor de servlets.

Viniendo de años escribiendo aplicaciones REST con Node.js, esto es, para mí, todo menos sensato. Cualquier excepción que genere un punto final del servicio debe regresar en la respuesta. No puedo entender por qué enviaría un redireccionamiento a lo que probablemente sea un consumidor de Angular o JQuery SPA que solo está buscando una respuesta y no puede o no tomará ninguna medida en un redireccionamiento.

Lo que quiero hacer es configurar un controlador de error global que pueda tomar cualquier excepción, ya sea a propósito desde un método de asignación de solicitudes o generado automáticamente por Spring (404 si no se encuentra ningún método de controlador para la firma de ruta de solicitud), y devolver un respuesta de error formateada estándar (400, 500, 503, 404) para el cliente sin redirecciones MVC. Específicamente, vamos a tomar el error, registrarlo en NoSQL con un UUID, luego devolver al cliente el código de error HTTP correcto con el UUID de la entrada de registro en el cuerpo JSON.

Los documentos han sido vagos sobre cómo hacer esto. Me parece que debes crear tu propia implementación ErrorController o usar ControllerAdvice de alguna manera, pero todos los ejemplos que he visto aún incluyen reenviar la respuesta a algún tipo de mapeo de error, lo que no ayuda. Otros ejemplos sugieren que debe enumerar todos los tipos de Excepción que desea manejar en lugar de solo enumerar “Throwable” y obtener todo.

¿Alguien puede decirme lo que me perdí, o apuntarme en la dirección correcta sobre cómo hacer esto sin sugerir en la cadena que Node.js sería más fácil de tratar?

Nueva respuesta (2016-04-20)

Usando Spring Boot 1.3.1.RELEASE

Nuevo paso 1: es fácil y menos intrusivo agregar las siguientes propiedades a la aplicación.propiedades:

 spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false 

¡Mucho más fácil que modificar la instancia de DispatcherServlet existente (como se muestra a continuación)! – JO ‘

Si trabaja con una aplicación RESTful completa, es muy importante desactivar la asignación automática de recursos estáticos ya que si está utilizando la configuración predeterminada de Spring Boot para manejar recursos estáticos, el manejador de recursos manejará la solicitud (se ordena por última vez y se asigna a / ** lo que significa que recoge cualquier solicitud que no haya sido manejada por ningún otro controlador en la aplicación) por lo que el servlet del distribuidor no tiene la oportunidad de lanzar una excepción.


Nueva respuesta (04/12/2015)

Usando Spring Boot 1.2.7.RELEASE

Nuevo Paso 1: encontré una forma mucho menos intrusiva de configurar el indicador “throExceptionIfNoHandlerFound”. Reemplace el código de reemplazo de DispatcherServlet a continuación (Paso 1) con esto en su clase de inicialización de la aplicación:

 @ComponentScan() @EnableAutoConfiguration public class MyApplication extends SpringBootServletInitializer { private static Logger LOG = LoggerFactory.getLogger(MyApplication.class); public static void main(String[] args) { ApplicationContext ctx = SpringApplication.run(MyApplication.class, args); DispatcherServlet dispatcherServlet = (DispatcherServlet)ctx.getBean("dispatcherServlet"); dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); } 

En este caso, estamos configurando el indicador en el DispatcherServlet existente, que preserva cualquier autoconfiguración mediante el marco Spring Boot.

Una cosa más que he encontrado: la anotación @EnableWebMvc es mortal para Spring Boot. Sí, esa anotación permite cosas como poder capturar todas las excepciones de controlador como se describe a continuación, pero también mata MUCHA la configuración automática útil que Spring Boot normalmente proporcionaría. Use esa anotación con extrema precaución cuando usa Spring Boot.


Respuesta Original:

Después de mucha más investigación y seguimiento de las soluciones publicadas aquí (¡gracias por la ayuda!) Y de una pequeña cantidad de rastreo de tiempo de ejecución en el código Spring, finalmente encontré una configuración que manejará todas las excepciones (no errores, pero siga leyendo) incluyendo 404s.

Paso 1: indica a SpringBoot que deje de utilizar MVC para las situaciones de “controlador no encontrado”. Queremos que Spring arroje una excepción en lugar de devolver al cliente una vista redirigida a “/ error”. Para hacer esto, necesita tener una entrada en una de sus clases de configuración:

 // NEW CODE ABOVE REPLACES THIS! (2015-12-04) @Configuration public class MyAppConfig { @Bean // Magic entry public DispatcherServlet dispatcherServlet() { DispatcherServlet ds = new DispatcherServlet(); ds.setThrowExceptionIfNoHandlerFound(true); return ds; } } 

La desventaja de esto es que reemplaza el servlet despachador predeterminado. Esto no ha sido un problema para nosotros todavía, sin efectos secundarios ni problemas de ejecución. Si va a hacer algo más con el servlet del operador por otros motivos, este es el lugar para hacerlo.

Paso 2: ahora que el arranque de spring lanzará una excepción cuando no se encuentre un controlador, esa excepción se puede manejar con cualquier otra en un controlador de excepciones unificado:

 @EnableWebMvc @ControllerAdvice public class ServiceExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(Throwable.class) @ResponseBody ResponseEntity handleControllerException(HttpServletRequest req, Throwable ex) { ErrorResponse errorResponse = new ErrorResponse(ex); if(ex instanceof ServiceException) { errorResponse.setDetails(((ServiceException)ex).getDetails()); } if(ex instanceof ServiceHttpException) { return new ResponseEntity(errorResponse,((ServiceHttpException)ex).getStatus()); } else { return new ResponseEntity(errorResponse,HttpStatus.INTERNAL_SERVER_ERROR); } } @Override protected ResponseEntity handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { Map responseBody = new HashMap<>(); responseBody.put("path",request.getContextPath()); responseBody.put("message","The URL you have reached is not in service at this time (404)."); return new ResponseEntity(responseBody,HttpStatus.NOT_FOUND); } ... } 

Tenga en cuenta que creo que la anotación “@EnableWebMvc” es importante aquí. Parece que nada de esto funciona sin eso. Y eso es todo: su aplicación de arranque de Spring ahora captará todas las excepciones, incluidas las 404, en la clase de controlador anterior y podrá hacerlas con ellos a su gusto.

Un último punto: no parece haber una forma de que esto capte errores lanzados. Tengo una idea absurda de usar aspectos para detectar errores y convertirlos en Excepciones que el código anterior puede manejar, pero todavía no he tenido tiempo de intentar implementar eso. Espero que esto ayude a alguien.

Cualquier comentario / corrección / mejora será apreciado.

Con Spring Boot 1.4+ se agregaron nuevas clases geniales para facilitar el manejo de excepciones que ayudan a eliminar el código repetitivo.

Se proporciona un nuevo @RestControllerAdvice para el manejo de excepciones, es una combinación de @ControllerAdvice y @ResponseBody . Puede eliminar @ResponseBody en el método @ExceptionHandler cuando use esta nueva anotación.

es decir

 @RestControllerAdvice public class GlobalControllerExceptionHandler { @ExceptionHandler(value = { Exception.class }) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiErrorResponse unknownException(Exception ex, WebRequest req) { return new ApiErrorResponse(...); } } 

Para manejar los errores 404, agregar anotaciones de @EnableWebMvc y lo siguiente para application.properties fue suficiente:
spring.mvc.throw-exception-if-no-handler-found=true

Puedes encontrar y jugar con las fonts aquí:
https://github.com/magiccrafter/spring-boot-exception-handling

Creo que ResponseEntityExceptionHandler cumple con sus requisitos. Una pieza de código de muestra para HTTP 400:

 @ControllerAdvice public class MyExceptionHandler extends ResponseEntityExceptionHandler { @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentNotValidException.class, HttpRequestMethodNotSupportedException.class}) public ResponseEntity badRequest(HttpServletRequest req, Exception exception) { // ... } } 

Puedes consultar esta publicación

¿Qué pasa con este código? Uso un mapeo de solicitud de respaldo para detectar errores 404.

 @Controller @ControllerAdvice public class ExceptionHandlerController { @ExceptionHandler(Exception.class) public ModelAndView exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) { //If exception has a ResponseStatus annotation then use its response code ResponseStatus responseStatusAnnotation = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class); return buildModelAndViewErrorPage(request, response, ex, responseStatusAnnotation != null ? responseStatusAnnotation.value() : HttpStatus.INTERNAL_SERVER_ERROR); } @RequestMapping("*") public ModelAndView fallbackHandler(HttpServletRequest request, HttpServletResponse response) throws Exception { return buildModelAndViewErrorPage(request, response, null, HttpStatus.NOT_FOUND); } private ModelAndView buildModelAndViewErrorPage(HttpServletRequest request, HttpServletResponse response, Exception ex, HttpStatus httpStatus) { response.setStatus(httpStatus.value()); ModelAndView mav = new ModelAndView("error.html"); if (ex != null) { mav.addObject("title", ex); } mav.addObject("content", request.getRequestURL()); return mav; } } 

Aunque esta es una pregunta anterior, me gustaría compartir mis pensamientos sobre esto. Espero que sea útil para algunos de ustedes.

Actualmente estoy construyendo una API REST que hace uso de Spring Boot 1.5.2.RELEASE con Spring Framework 4.3.7.RELEASE. Uso el enfoque de configuración de Java (a diferencia de la configuración XML). Además, mi proyecto utiliza un mecanismo global de manejo de excepciones utilizando la anotación @RestControllerAdvice (ver más adelante a continuación).

Mi proyecto tiene los mismos requisitos que el suyo: quiero que mi API REST devuelva un HTTP 404 Not Found con una carga útil JSON en la respuesta HTTP al cliente API cuando intenta enviar una solicitud a una URL que no existe. En mi caso, la carga útil de JSON se ve así (que difiere claramente del valor predeterminado de Spring Boot, por cierto):

 { "code": 1000, "message": "No handler found for your request.", "timestamp": "2017-11-20T02:40:57.628Z" } 

Finalmente lo hice funcionar. Estas son las principales tareas que debe hacer en breve:

  • Asegúrese de que la NoHandlerFoundException se NoHandlerFoundException si los clientes de la API llaman a URLS para los que no existe un método de control (consulte el paso 1 a continuación).
  • Cree una clase de error personalizada (en mi caso, ApiError ) que contenga todos los datos que se deben devolver al cliente API (consulte el paso 2).
  • Cree un manejador de excepciones que reaccione en NoHandlerFoundException y devuelva un mensaje de error adecuado al cliente API (consulte el paso 3).
  • Escriba una prueba y asegúrese de que funciona (vea el paso 4).

Ok, ahora a los detalles:

Paso 1: configurar application.properties

Tuve que agregar las siguientes dos configuraciones al archivo application.properties del proyecto:

 spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false 

Esto garantiza que la NoHandlerFoundException se NoHandlerFoundException en los casos en que un cliente intente acceder a una URL para la cual no existe un método de controlador que pueda gestionar la solicitud.

Paso 2: crea una clase para errores de API

Hice una clase similar a la sugerida en este artículo en el blog de Eugen Paraschiv. Esta clase representa un error API. Esta información se envía al cliente en el cuerpo de respuesta HTTP en caso de error.

 public class ApiError { private int code; private String message; private Instant timestamp; public ApiError(int code, String message) { this.code = code; this.message = message; this.timestamp = Instant.now(); } public ApiError(int code, String message, Instant timestamp) { this.code = code; this.message = message; this.timestamp = timestamp; } // Getters and setters here... } 

Paso 3: Crear / Configurar un controlador de excepción global

Utilizo la siguiente clase para manejar excepciones (por simplicidad, he eliminado instrucciones de importación, código de registro y algunas otras partes de código no relevantes):

 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ApiError noHandlerFoundException( NoHandlerFoundException ex) { int code = 1000; String message = "No handler found for your request."; return new ApiError(code, message); } // More exception handlers here ... } 

Paso 4: Escribe una prueba

Quiero asegurarme de que la API siempre devuelva los mensajes de error correctos al cliente que realiza la llamada, incluso en el caso de una falla. Por lo tanto, escribí una prueba como esta:

 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SprintBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @ActiveProfiles("dev") public class GlobalExceptionHandlerIntegrationTest { public static final String ISO8601_DATE_REGEX = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$"; @Autowired private MockMvc mockMvc; @Test @WithMockUser(roles = "DEVICE_SCAN_HOSTS") public void invalidUrl_returnsHttp404() throws Exception { RequestBuilder requestBuilder = getGetRequestBuilder("/does-not-exist"); mockMvc.perform(requestBuilder) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.code", is(1000))) .andExpect(jsonPath("$.message", is("No handler found for your request."))) .andExpect(jsonPath("$.timestamp", RegexMatcher.matchesRegex(ISO8601_DATE_REGEX))); } private RequestBuilder getGetRequestBuilder(String url) { return MockMvcRequestBuilders .get(url) .accept(MediaType.APPLICATION_JSON); } 

La @ActiveProfiles("dev") puede ser dejada de lado. Lo uso solo cuando trabajo con diferentes perfiles. El RegexMatcher es un emparejador personalizado de Hamcrest que utilizo para manejar mejor los campos de marcas de tiempo. Aquí está el código (lo encontré aquí ):

 public class RegexMatcher extends TypeSafeMatcher { private final String regex; public RegexMatcher(final String regex) { this.regex = regex; } @Override public void describeTo(final Description description) { description.appendText("matches regular expression=`" + regex + "`"); } @Override public boolean matchesSafely(final String string) { return string.matches(regex); } // Matcher method you can call on this matcher class public static RegexMatcher matchesRegex(final String string) { return new RegexMatcher(regex); } } 

Algunas notas adicionales de mi lado:

  • En muchas otras publicaciones en StackOverflow, la gente sugirió establecer la anotación @EnableWebMvc . Esto no fue necesario en mi caso.
  • Este enfoque funciona bien con MockMvc (ver prueba anterior).

Por defecto, Spring Boot proporciona a json los detalles del error.

 curl -v localhost:8080/greet | json_pp [...] < HTTP/1.1 400 Bad Request [...] { "timestamp" : 1413313361387, "exception" : "org.springframework.web.bind.MissingServletRequestParameterException", "status" : 400, "error" : "Bad Request", "path" : "/greet", "message" : "Required String parameter 'name' is not present" } 

También funciona para todo tipo de errores de asignación de solicitudes. Consulte este artículo http://www.jayway.com/2014/10/19/spring-boot-error-responses/

Si desea crearlo, regístrese en NoSQL. Puede crear @ControllerAdvice donde lo registraría y luego volvería a lanzar la excepción. Hay un ejemplo en la documentación https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

Para los controladores REST, recomendaría usar Zalando Problem Spring Web .

https://github.com/zalando/problem-spring-web

Si Spring Boot tiene como objective incorporar algo de autoconfiguración, esta biblioteca hace más para el manejo de excepciones. Solo necesita agregar la dependencia:

  org.zalando problem-spring-web LATEST  

Y luego defina uno o más rasgos de asesoramiento para sus excepciones (o use los que se proporcionan por defecto)

 public interface NotAcceptableAdviceTrait extends AdviceTrait { @ExceptionHandler default ResponseEntity handleMediaTypeNotAcceptable( final HttpMediaTypeNotAcceptableException exception, final NativeWebRequest request) { return Responses.create(Status.NOT_ACCEPTABLE, exception, request); } } 

Luego puede definir el consejo del controlador para el manejo de excepciones como:

 @ControllerAdvice class ExceptionHandling implements MethodNotAllowedAdviceTrait, NotAcceptableAdviceTrait { } 

Solución con dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); y @EnableWebMvc @ControllerAdvice funcionó para mí con Spring Boot 1.3.1, mientras que no funcionaba en 1.2.7

@RestControllerAdvice es una nueva característica de Spring Framework 4.3 para manejar Exception con RestfulApi mediante una solución de interés transversal:

  package com.khan.vaquar.exception; import javax.servlet.http.HttpServletRequest; import org.owasp.esapi.errors.IntrusionException; import org.owasp.esapi.errors.ValidationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; import com.fasterxml.jackson.core.JsonProcessingException; import com.khan.vaquar.domain.ErrorResponse; /** * Handles exceptions raised through requests to spring controllers. **/ @RestControllerAdvice public class RestExceptionHandler { private static final String TOKEN_ID = "tokenId"; private static final Logger log = LoggerFactory.getLogger(RestExceptionHandler.class); /** * Handles InstructionExceptions from the rest controller. * * @param e IntrusionException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IntrusionException.class) public ErrorResponse handleIntrusionException(HttpServletRequest request, IntrusionException e) { log.warn(e.getLogMessage(), e); return this.handleValidationException(request, new ValidationException(e.getUserMessage(), e.getLogMessage())); } /** * Handles ValidationExceptions from the rest controller. * * @param e ValidationException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = ValidationException.class) public ErrorResponse handleValidationException(HttpServletRequest request, ValidationException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); if (e.getUserMessage().contains("Token ID")) { tokenId = ""; } return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getUserMessage()); } /** * Handles JsonProcessingExceptions from the rest controller. * * @param e JsonProcessingException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = JsonProcessingException.class) public ErrorResponse handleJsonProcessingException(HttpServletRequest request, JsonProcessingException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getOriginalMessage()); } /** * Handles IllegalArgumentExceptions from the rest controller. * * @param e IllegalArgumentException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IllegalArgumentException.class) public ErrorResponse handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getMessage()); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = UnsupportedOperationException.class) public ErrorResponse handleUnsupportedOperationException(HttpServletRequest request, UnsupportedOperationException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getMessage()); } /** * Handles MissingServletRequestParameterExceptions from the rest controller. * * @param e MissingServletRequestParameterException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MissingServletRequestParameterException.class) public ErrorResponse handleMissingServletRequestParameterException( HttpServletRequest request, MissingServletRequestParameterException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getMessage()); } /** * Handles NoHandlerFoundExceptions from the rest controller. * * @param e NoHandlerFoundException * @return error response POJO */ @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(value = NoHandlerFoundException.class) public ErrorResponse handleNoHandlerFoundException(HttpServletRequest request, NoHandlerFoundException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.NOT_FOUND.value(), e.getClass().getSimpleName(), "The resource " + e.getRequestURL() + " is unavailable"); } /** * Handles all remaining exceptions from the rest controller. * * This acts as a catch-all for any exceptions not handled by previous exception handlers. * * @param e Exception * @return error response POJO */ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(value = Exception.class) public ErrorResponse handleException(HttpServletRequest request, Exception e) { String tokenId = request.getParameter(TOKEN_ID); log.error(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getClass().getSimpleName(), "An internal error occurred"); } }