Gestione delle eccezioni del servizio REST Spring Boot

Sto cercando di creare un server di servizi REST su larga scala. Stiamo utilizzando Spring Boot 1.2.1 Spring 4.1.5 e Java 8. I nostri controller implementano @RestController e le annotazioni standard @RequestMapping.

Il mio problema è che Spring Boot imposta un reindirizzamento predefinito per eccezioni /error . Dai documenti:

Spring Boot fornisce un / error mapping per default che gestisce tutti gli errori in modo sensibile, ed è registrato come una pagina di errore ‘globale’ nel container servlet.

Venendo da anni a scrivere applicazioni REST con Node.js, questo per me è tutt’altro che ragionevole. Qualsiasi eccezione generata da un endpoint del servizio deve essere restituita nella risposta. Non riesco a capire perché invii un reindirizzamento a ciò che è più probabile un consumatore di Angular o JQuery SPA, che è solo alla ricerca di una risposta e non può o non intraprenderà alcuna azione su un reindirizzamento.

Quello che voglio fare è impostare un gestore di errori globale che possa prendere qualsiasi eccezione – intenzionalmente generata da un metodo di mapping delle richieste o generata automaticamente da Spring (404 se non viene trovato alcun metodo di gestione per la firma del percorso della richiesta) e restituire un risposta all’errore formattata standard (400, 500, 503, 404) al client senza alcun reindirizzamento MVC. In particolare, prenderemo l’errore, lo registreremo su NoSQL con un UUID, quindi restituiremo al client il codice di errore HTTP corretto con l’UUID della voce di registro nel corpo JSON.

I documenti sono stati vaghi su come farlo. Mi sembra che devi creare la tua implementazione ErrorController o usare ControllerAdvice in qualche modo, ma tutti gli esempi che ho visto includono ancora l’inoltro della risposta a una sorta di mapping degli errori, il che non aiuta. Altri esempi suggeriscono che dovresti elencare tutti i tipi di eccezioni che desideri gestire invece di elencare semplicemente “Throwable” e ottenere tutto.

Qualcuno può dirmi cosa mi è mancato o indicarmi la giusta direzione su come farlo senza suggerire la catena che Node.js sarebbe più facile da gestire?

Nuova risposta (20/04/2016)

Utilizzo di Spring Boot 1.3.1.RELEASE

Nuovo passaggio 1 – È facile e meno intrusivo aggiungere le seguenti proprietà a application.properties:

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

Molto più semplice della modifica dell’istanza DispatcherServlet esistente (come sotto)! – JO ‘

Se si lavora con un’applicazione RESTful completa, è molto importante disabilitare la mapping automatica delle risorse statiche poiché se si sta utilizzando la configurazione predefinita di Spring Boot per la gestione delle risorse statiche, il gestore delle risorse gestirà la richiesta (è ordinata per ultima e mappata a / ** il che significa che preleva le richieste che non sono state gestite da nessun altro gestore nell’applicazione) in modo che il servlet del dispatcher non abbia la possibilità di lanciare un’eccezione.


Nuova risposta (2015-12-04)

Utilizzo di Spring Boot 1.2.7.RELEASE

Nuovo passaggio 1: ho trovato un modo molto meno intrusivo per impostare il flag “throExceptionIfNoHandlerFound”. Sostituisci il codice di sostituzione di DispatcherServlet sotto (Passaggio 1) con questo nella class di inizializzazione dell’applicazione:

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

In questo caso, impostiamo il flag sul DispatcherServlet esistente, che preserva qualsiasi configurazione automatica dal framework Spring Boot.

Un’altra cosa che ho trovato – l’annotazione @EnableWebMvc è letale per Spring Boot. Sì, quell’annotazione abilita cose come essere in grado di catturare tutte le eccezioni del controller come descritto di seguito, ma uccide anche MOLTO l’utile configurazione automatica che Spring Boot normalmente fornirebbe. Usa quell’annotazione con estrema caucanvas quando usi Spring Boot.


Risposta originale:

Dopo molte più ricerche e dopo aver seguito le soluzioni pubblicate qui (grazie per l’aiuto!) E nessuna piccola quantità di traccia di runtime nel codice Spring, ho finalmente trovato una configurazione che gestirà tutte le eccezioni (non errori, ma continua a leggere) compresi 404s.

Passaggio 1: indica a SpringBoot di interrompere l’utilizzo di MVC per le situazioni di “gestore non trovato”. Vogliamo che Spring lanci un’eccezione invece di restituire al client un reindirizzamento della vista su “/ error”. Per fare ciò, devi avere una voce in una delle tue classi di configurazione:

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

Lo svantaggio di questo è che sostituisce il servlet di dispatcher predefinito. Questo non è stato un problema per noi ancora, senza effetti collaterali o problemi di esecuzione. Se hai intenzione di fare qualcos’altro con il servlet dispatcher per altri motivi, questo è il posto giusto per farli.

Passo 2 – Ora che l’avvio primaverile genera un’eccezione quando non viene trovato alcun gestore, tale eccezione può essere gestita con qualsiasi altra in un gestore eccezioni unificato:

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

Tieni presente che penso che l’annotazione “@EnableWebMvc” sia significativa qui. Sembra che nessuno di questi lavori senza di esso. E questo è tutto: la tua app di avvio Spring ora cattura tutte le eccezioni, inclusi 404, nella class di gestori sopra e puoi farne a loro a tuo piacimento.

Un ultimo punto: non sembra essere un modo per ottenere questo per catturare gli errori lanciati. Ho una bizzarra idea di utilizzare gli aspetti per catturare gli errori e trasformarli in Eccezioni che il codice precedente può quindi gestire, ma non ho ancora avuto il tempo di provare effettivamente a implementarlo. Spero che questo aiuti qualcuno.

Saranno apprezzati eventuali commenti / correzioni / miglioramenti.

Con Spring Boot 1.4+ sono state aggiunte nuove fantastiche classi per una gestione delle eccezioni più semplice che aiuta a rimuovere il codice boilerplate.

Viene fornito un nuovo @RestControllerAdvice per la gestione delle eccezioni, è la combinazione di @ControllerAdvice e @ResponseBody . Puoi rimuovere @ResponseBody sul metodo @ExceptionHandler quando usi questa nuova annotazione.

vale a dire

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

Per la gestione degli errori 404 è stato sufficiente aggiungere @EnableWebMvc annotazione @EnableWebMvc e quanto segue a application.properties:
spring.mvc.throw-exception-if-no-handler-found=true

Puoi trovare e giocare con le fonti qui:
https://github.com/magiccrafter/spring-boot-exception-handling

Penso che ResponseEntityExceptionHandler soddisfi i tuoi requisiti. Un esempio di codice per 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) { // ... } } 

Puoi controllare questo post

Che dire di questo codice? Io uso una mapping richiesta fallback per catturare errori 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; } } 

Anche se questa è una domanda più vecchia, vorrei condividere le mie opinioni su questo. Spero che possa essere d’aiuto a qualcuno di voi.

Attualmente sto costruendo una API REST che fa uso di Spring Boot 1.5.2.RELEASE con Spring Framework 4.3.7.RELEASE. Io uso l’approccio Java Config (al contrario della configurazione XML). Inoltre, il mio progetto utilizza un meccanismo di gestione delle eccezioni globale usando l’annotazione @RestControllerAdvice (vedi più avanti).

Il mio progetto ha gli stessi requisiti del tuo: Voglio che la mia API REST restituisca un HTTP 404 Not Found con un payload JSON associato nella risposta HTTP al client API quando tenta di inviare una richiesta a un URL che non esiste. Nel mio caso, il payload JSON è simile a questo (che si differenzia chiaramente dal default di Spring Boot, btw.):

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

Finalmente l’ho fatto funzionare. Ecco le attività principali che devi svolgere in breve:

  • Assicurati che la NoHandlerFoundException venga generata se i client API chiamano URLS per i quali non esiste alcun metodo di gestione (vedere il Passaggio 1 di seguito).
  • Creare una class di errore personalizzata (nel mio caso ApiError ) che contiene tutti i dati che devono essere restituiti al client API (vedere il passaggio 2).
  • Creare un gestore di eccezioni che reagisce a NoHandlerFoundException e restituisce un messaggio di errore corretto al client API (vedere il passaggio 3).
  • Scrivi un test per questo e assicurati che funzioni (vedi il punto 4).

Ok, ora ai dettagli:

Passaggio 1: configurare application.properties

Ho dovuto aggiungere le seguenti due impostazioni di configurazione al file application.properties del progetto:

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

Ciò NoHandlerFoundException la NoHandlerFoundException venga generata nei casi in cui un client tenta di accedere a un URL per il quale non esiste alcun metodo di controllo che sia in grado di gestire la richiesta.

Passaggio 2: creare una class per gli errori API

Ho fatto una lezione simile a quella suggerita in questo articolo sul blog di Eugen Paraschiv. Questa class rappresenta un errore API. Questa informazione viene inviata al client nel corpo di risposta HTTP in caso di errore.

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

Passaggio 3: Creare / Configurare un gestore globale delle eccezioni

Uso la seguente class per gestire le eccezioni (per semplicità, ho rimosso le istruzioni di importazione, il codice di registrazione e alcune altre parti di codice non rilevanti):

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

Passaggio 4: scrivere un test

Voglio assicurarmi che l’API restituisca sempre i messaggi di errore corretti al client chiamante, anche in caso di errore. Quindi, ho scritto un test come questo:

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

L’ @ActiveProfiles("dev") può essere lasciata fuori. Lo uso solo mentre lavoro con profili diversi. RegexMatcher è un matcher Hamcrest personalizzato che utilizzo per gestire meglio i campi di timestamp. Ecco il codice (l’ho trovato qui ):

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

Alcune note ulteriori dalla mia parte:

  • In molti altri post su StackOverflow, le persone hanno suggerito di impostare l’annotazione @EnableWebMvc . Questo non era necessario nel mio caso.
  • Questo approccio funziona bene con MockMvc (vedi test sopra).

Di default Spring Boot dà a JSON i dettagli dell’errore.

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

Funziona anche per tutti i tipi di errori di mapping delle richieste. Controlla questo articolo http://www.jayway.com/2014/10/19/spring-boot-error-responses/

Se si desidera creare il registro su NoSQL. Puoi creare @ControllerAdvice in cui dovresti loggarlo e poi rilanciare l'eccezione. C'è un esempio nella documentazione https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

Per i controller REST, consiglierei di usare Zalando Problem Spring Web .

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

Se Spring Boot mira ad incorporare alcune configurazioni automatiche, questa libreria fa di più per la gestione delle eccezioni. Hai solo bisogno di aggiungere la dipendenza:

  org.zalando problem-spring-web LATEST  

E poi definisci uno o più tratti di avviso per le tue eccezioni (o usa quelle fornite di default)

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

Quindi è ansible definire il consiglio del controller per la gestione delle eccezioni come:

 @ControllerAdvice class ExceptionHandling implements MethodNotAllowedAdviceTrait, NotAcceptableAdviceTrait { } 

Soluzione con dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); e @EnableWebMvc @ControllerAdvice funzionato per me con Spring Boot 1.3.1, mentre non funzionava su 1.2.7

@RestControllerAdvice è una nuova funzionalità di Spring Framework 4.3 per gestire Eccezioni con RestfulApi da una soluzione di interesse trasversale:

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