handling example error custom controlleradvice java spring rest exception-handling spring-boot

java - error - spring rest exception handler example



Manejo de excepciones del servicio Spring Boot REST (10)

¿Qué hay de este código? Utilizo una asignación de solicitud de reserva 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; } }

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

Mi problema es que Spring Boot configura una redirección predeterminada para las excepciones del controlador a /error . De los documentos:

Spring Boot proporciona un mapeo / error por defecto que maneja todos los errores de manera sensata, y está registrado 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 de servicio debería volver en la respuesta. No puedo entender por qué enviaría una redirección 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 acción en una redirección.

Lo que quiero hacer es configurar un controlador de errores global que pueda tomar cualquier excepción, ya sea arrojada intencionalmente desde un método de mapeo de solicitud o generada 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 con formato estándar (400, 500, 503, 404) al cliente sin ningún redireccionamiento 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 tiene que crear su propia implementación de ErrorController o usar ControllerAdvice de alguna manera, pero todos los ejemplos que he visto todavía incluyen reenviar la respuesta a algún tipo de mapeo de errores, lo que no ayuda. Otros ejemplos sugieren que tendría que enumerar todos los tipos de Excepción que desea manejar en lugar de simplemente enumerar "Throwable" y obtener todo.

¿Alguien puede decirme lo que me perdí o señalarme en la dirección correcta cómo hacerlo sin sugerir la cadena con la que Node.js sería más fácil tratar?


@RestControllerAdvice es una nueva característica de Spring Framework 4.3 para manejar Exception with RestfulApi mediante una solución de preocupación 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 = "<OMITTED>"; } 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"); } }


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

Actualmente estoy construyendo una API REST que utiliza Spring Boot 1.5.2.RELEASE con Spring Framework 4.3.7.RELEASE. Yo uso el enfoque de Java Config (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).

Mi proyecto tiene los mismos requisitos que el suyo: quiero que mi API REST devuelva un HTTP 404 Not Found con una carga JSON que lo acompaña 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 claramente difiere 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 tareas principales que debe hacer en resumen:

  • Asegúrese de que se NoHandlerFoundException si los clientes API llaman a URLS para las cuales no existe un método de controlador (consulte el Paso 1 a continuación).
  • Cree una clase de error personalizada (en mi caso, ApiError ) que contiene todos los datos que deben devolverse al cliente API (consulte el paso 2).
  • Cree un controlador de excepción 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 opciones de configuración al archivo application.properties del proyecto:

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

Esto asegura que la NoHandlerFoundException produce en los casos en que un cliente intenta acceder a una URL para la cual no existe ningún método de controlador que pueda manejar la solicitud.

Paso 2: crear 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 de 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 las declaraciones de importación, el código de registro y algunos otros fragmentos 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 devuelve los mensajes de error correctos al cliente que realiza la llamada, incluso en caso de 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 @ActiveProfiles("dev") . Lo uso solo mientras trabajo con diferentes perfiles. El RegexMatcher es un emparejador Hamcrest personalizado que uso para manejar mejor los campos de marca de tiempo. Aquí está el código (lo encontré here ):

public class RegexMatcher extends TypeSafeMatcher<String> { 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 parte:

  • En muchas otras publicaciones en , las personas sugirieron configurar la anotación @EnableWebMvc . Esto no fue necesario en mi caso.
  • Este enfoque funciona bien con MockMvc (ver prueba anterior).

Con Spring Boot 1.4+ se agregaron nuevas clases interesantes para un manejo de excepciones más fácil que ayuda 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 la anotación @EnableWebMvc y lo siguiente a application.properties fue suficiente:
spring.mvc.throw-exception-if-no-handler-found=true

Puedes encontrar y jugar con las fuentes 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<Object> badRequest(HttpServletRequest req, Exception exception) { // ... } }

Puedes consultar esta post


Para las personas que desean responder de acuerdo con el código de estado http, puede usar la forma ErrorController :

@Controller public class CustomErrorController extends BasicErrorController { public CustomErrorController(ServerProperties serverProperties) { super(new DefaultErrorAttributes(), serverProperties.getError()); } @Override public ResponseEntity error(HttpServletRequest request) { HttpStatus status = getStatus(request); if (status.equals(HttpStatus.INTERNAL_SERVER_ERROR)){ return ResponseEntity.status(status).body(ResponseBean.SERVER_ERROR); }else if (status.equals(HttpStatus.BAD_REQUEST)){ return ResponseEntity.status(status).body(ResponseBean.BAD_REQUEST); } return super.error(request); } }

El ResponseBean aquí es mi pojo personalizado para la respuesta.


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

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

Si Spring Boot tiene como objetivo incrustar alguna configuración automática, esta biblioteca hace más para el manejo de excepciones. Solo necesita agregar la dependencia:

<dependency> <groupId>org.zalando</groupId> <artifactId>problem-spring-web</artifactId> <version>LATEST</version> </dependency>

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

public interface NotAcceptableAdviceTrait extends AdviceTrait { @ExceptionHandler default ResponseEntity<Problem> 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 { }


Por defecto, Spring Boot proporciona json con detalles de 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 mapeo de solicitudes. Consulte este artículo http://www.jayway.com/2014/10/19/spring-boot-error-responses/

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


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


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.

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

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

Si trabaja con una aplicación RESTful completa, es muy importante deshabilitar 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 controlador de recursos manejará la solicitud (se ordena al final 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) para que el servlet del despachador no tenga 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 la clase de inicialización de su 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 conserva cualquier configuración automática por parte del 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 del controlador como se describe a continuación, pero también elimina MUCHA de la útil configuración automática que Spring Boot proporcionaría normalmente. Use esa anotación con extrema precaución cuando use Spring Boot.

Respuesta original

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

Paso 1: dígale a SpringBoot que deje de usar MVC para 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, debe 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 del despachador predeterminado. Esto aún no ha sido un problema para nosotros, sin efectos secundarios ni problemas de ejecución. Si va a hacer algo más con el servlet del despachador por otros motivos, este es el lugar para hacerlo.

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

@EnableWebMvc @ControllerAdvice public class ServiceExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(Throwable.class) @ResponseBody ResponseEntity<Object> 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<Object>(errorResponse,((ServiceHttpException)ex).getStatus()); } else { return new ResponseEntity<Object>(errorResponse,HttpStatus.INTERNAL_SERVER_ERROR); } } @Override protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { Map<String,String> 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<Object>(responseBody,HttpStatus.NOT_FOUND); } ... }

Tenga en cuenta que creo que la anotación "@EnableWebMvc" es importante aquí. Parece que nada de esto funciona sin él. Y eso es todo: su aplicación de arranque Spring ahora detectará todas las excepciones, incluidos los 404, en la clase de controlador anterior y puede hacer lo que quiera con ellos.

Un último punto: no parece haber una forma de hacer que esto atrape los errores arrojados. Tengo una extraña idea de usar aspectos para detectar errores y convertirlos en Excepciones con las que el código anterior puede lidiar, pero aún no he tenido tiempo de intentar implementarlo. Espero que esto ayude a alguien.

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