Gestione degli errori per REST con Spring

REST Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Panoramica

Questo tutorial illustrerà come implementare la gestione delle eccezioni con Spring per un'API REST. Avremo anche una panoramica storica e vedremo quali nuove opzioni sono state introdotte nelle diverse versioni.

Prima di Spring 3.2, i due approcci principali alla gestione delle eccezioni in un'applicazione Spring MVC erano HandlerExceptionResolver o l' annotazione @ExceptionHandler . Entrambi hanno alcuni chiari svantaggi.

Dalla 3.2, abbiamo avuto l' annotazione @ControllerAdvice per affrontare i limiti delle due soluzioni precedenti e per promuovere una gestione unificata delle eccezioni in tutta un'applicazione.

Ora Spring 5 introduce la classe ResponseStatusException , un modo rapido per la gestione degli errori di base nelle nostre API REST.

Tutti questi hanno una cosa in comune: affrontano molto bene la separazione delle preoccupazioni . L'app può generare eccezioni normalmente per indicare un errore di qualche tipo, che verrà quindi gestito separatamente.

Infine, vedremo cosa porta in tavola Spring Boot e come possiamo configurarlo per soddisfare le nostre esigenze.

2. Soluzione 1: @ExceptionHandler a livello di controller

La prima soluzione funziona a livello di @Controller . Definiremo un metodo per gestire le eccezioni e annotarlo con @ExceptionHandler :

public class FooController{ //... @ExceptionHandler({ CustomException1.class, CustomException2.class }) public void handleException() { // } }

Questo approccio presenta un grave inconveniente: T ha @ExceptionHandler metodo annotato è attivo solo per quel particolare controller , non globalmente per l'intera applicazione. Ovviamente, aggiungerlo a ogni controller lo rende non adatto per un meccanismo generale di gestione delle eccezioni.

Possiamo aggirare questa limitazione facendo in modo che tutti i controller estendano una classe Controller di base.

Tuttavia, questa soluzione può essere un problema per le applicazioni in cui, per qualsiasi motivo, ciò non è possibile. Ad esempio, i controller possono già estendersi da un'altra classe base, che può essere in un altro jar o non direttamente modificabili, oppure potrebbero non essere modificabili direttamente.

Successivamente, esamineremo un altro modo per risolvere il problema di gestione delle eccezioni, uno che è globale e non include alcuna modifica agli artefatti esistenti come i controller.

3. Soluzione 2: HandlerExceptionResolver

La seconda soluzione è definire un HandlerExceptionResolver. Questo risolverà qualsiasi eccezione generata dall'applicazione. Ci consentirà inoltre di implementare un meccanismo di gestione delle eccezioni uniforme nella nostra API REST.

Prima di optare per un resolver personalizzato, esaminiamo le implementazioni esistenti.

3.1. ExceptionHandlerExceptionResolver

Questo resolver è stato introdotto nella Spring 3.1 ed è abilitato per impostazione predefinita nel DispatcherServlet . Questo è in realtà il componente principale del funzionamento del meccanismo @ ExceptionHandler presentato in precedenza.

3.2. DefaultHandlerExceptionResolver

Questo resolver è stato introdotto nella primavera 3.0 ed è abilitato per impostazione predefinita nel DispatcherServlet .

Viene utilizzato per risolvere le eccezioni Spring standard ai codici di stato HTTP corrispondenti, ovvero i codici di stato Errore client 4xx e Errore server 5xx . Ecco l' elenco completo delle eccezioni di primavera che gestisce e come si associano ai codici di stato.

Anche se imposta correttamente il codice di stato della risposta, una limitazione è che non imposta nulla sul corpo della risposta. E per un'API REST - il codice di stato non è davvero un'informazione sufficiente da presentare al client - anche la risposta deve avere un corpo, per consentire all'applicazione di fornire ulteriori informazioni sull'errore.

Questo può essere risolto configurando la risoluzione della vista e il contenuto degli errori di rendering tramite ModelAndView , ma la soluzione chiaramente non è ottimale. Ecco perché la Spring 3.2 ha introdotto un'opzione migliore di cui parleremo in una sezione successiva.

3.3. ResponseStatusExceptionResolver

Questo resolver è stato introdotto anche nella Spring 3.0 ed è abilitato per impostazione predefinita nel DispatcherServlet .

La sua responsabilità principale è utilizzare l' annotazione @ResponseStatus disponibile sulle eccezioni personalizzate e mappare queste eccezioni ai codici di stato HTTP.

Tale eccezione personalizzata può essere simile a:

@ResponseStatus(value = HttpStatus.NOT_FOUND) public class MyResourceNotFoundException extends RuntimeException { public MyResourceNotFoundException() { super(); } public MyResourceNotFoundException(String message, Throwable cause) { super(message, cause); } public MyResourceNotFoundException(String message) { super(message); } public MyResourceNotFoundException(Throwable cause) { super(cause); } }

Come DefaultHandlerExceptionResolver , questo resolver è limitato nel modo in cui gestisce il corpo della risposta: mappa il codice di stato sulla risposta, ma il corpo è ancora nullo.

3.4. SimpleMappingExceptionResolver e AnnotationMethodHandlerExceptionResolver

Il SimpleMappingExceptionResolver è stato intorno per un bel po 'di tempo. Deriva dal vecchio modello Spring MVC e non è molto rilevante per un servizio REST. Fondamentalmente lo usiamo per mappare i nomi delle classi di eccezioni per visualizzare i nomi.

L'AnnotationMethodHandlerExceptionResolver è stato introdotto nella primavera del 3,0 per gestire le eccezioni attraverso la @ExceptionHandler di annotazione, ma è stato deprecato da ExceptionHandlerExceptionResolver a partire dalla primavera 3.2.

3.5. HandlerExceptionResolver personalizzato

La combinazione di DefaultHandlerExceptionResolver e ResponseStatusExceptionResolver fa molto per fornire un buon meccanismo di gestione degli errori per un servizio Spring RESTful. Lo svantaggio è, come accennato prima, nessun controllo sul corpo della risposta.

Idealmente, vorremmo essere in grado di produrre JSON o XML, a seconda del formato richiesto dal cliente (tramite l' intestazione Accept ).

Questo da solo giustifica la creazione di un nuovo risolutore di eccezioni personalizzato :

@Component public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver { @Override protected ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (ex instanceof IllegalArgumentException) { return handleIllegalArgument( (IllegalArgumentException) ex, response, handler); } ... } catch (Exception handlerException) { logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException); } return null; } private ModelAndView handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response) throws IOException { response.sendError(HttpServletResponse.SC_CONFLICT); String accept = request.getHeader(HttpHeaders.ACCEPT); ... return new ModelAndView(); } }

Un dettaglio da notare qui è che abbiamo accesso alla richiesta stessa, quindi possiamo considerare il valore dell'intestazione Accept inviata dal client.

Ad esempio, se il client richiede application / json , in caso di una condizione di errore, vorremmo assicurarci di restituire un corpo della risposta codificato con application / json .

L'altro importante dettaglio di implementazione è che restituiamo un ModelAndView : questo è il corpo della risposta e ci consentirà di impostare tutto ciò che è necessario su di esso.

Questo approccio è un meccanismo coerente e facilmente configurabile per la gestione degli errori di un servizio Spring REST.

Tuttavia, ha delle limitazioni: interagisce con HtttpServletResponse di basso livello e si adatta al vecchio modello MVC che usa ModelAndView , quindi c'è ancora spazio per miglioramenti.

4. Soluzione 3: @ControllerAdvice

Spring 3.2 offre il supporto per un @ExceptionHandler globale con l' annotazione @ControllerAdvice .

Ciò abilita un meccanismo che si allontana dal vecchio modello MVC e fa uso di ResponseEntity insieme all'indipendenza dai tipi e alla flessibilità di @ExceptionHandler :

@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = { IllegalArgumentException.class, IllegalStateException.class }) protected ResponseEntity handleConflict( RuntimeException ex, WebRequest request) { String bodyOfResponse = "This should be application specific"; return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request); } }

The@ControllerAdvice annotation allows us to consolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

The actual mechanism is extremely simple but also very flexible:

  • It gives us full control over the body of the response as well as the status code.
  • It provides mapping of several exceptions to the same method, to be handled together.
  • It makes good use of the newer RESTful ResposeEntity response.

One thing to keep in mind here is to match the exceptions declared with @ExceptionHandler to the exception used as the argument of the method.

If these don't match, the compiler will not complain — no reason it should — and Spring will not complain either.

However, when the exception is actually thrown at runtime, the exception resolving mechanism will fail with:

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...] HandlerMethod details: ...

5. Solution 4: ResponseStatusException (Spring 5 and Above)

Spring 5 introduced the ResponseStatusException class.

We can create an instance of it providing an HttpStatus and optionally a reason and a cause:

@GetMapping(value = "/{id}") public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) { try { Foo resourceById = RestPreconditions.checkFound(service.findOne(id)); eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response)); return resourceById; } catch (MyResourceNotFoundException exc) { throw new ResponseStatusException( HttpStatus.NOT_FOUND, "Foo Not Found", exc); } }

What are the benefits of using ResponseStatusException?

  • Excellent for prototyping: We can implement a basic solution quite fast.
  • One type, multiple status codes: One exception type can lead to multiple different responses. This reduces tight coupling compared to the @ExceptionHandler.
  • We won't have to create as many custom exception classes.
  • We have more control over exception handling since the exceptions can be created programmatically.

And what about the tradeoffs?

  • There's no unified way of exception handling: It's more difficult to enforce some application-wide conventions as opposed to @ControllerAdvice, which provides a global approach.
  • Code duplication: We may find ourselves replicating code in multiple controllers.

We should also note that it's possible to combine different approaches within one application.

For example, we can implement a @ControllerAdvice globally but also ResponseStatusExceptions locally.

However, we need to be careful: If the same exception can be handled in multiple ways, we may notice some surprising behavior. A possible convention is to handle one specific kind of exception always in one way.

For more details and further examples, see our tutorial on ResponseStatusException.

6. Handle the Access Denied in Spring Security

The Access Denied occurs when an authenticated user tries to access resources that he doesn't have enough authorities to access.

6.1. MVC — Custom Error Page

First, let's look at the MVC style of the solution and see how to customize an error page for Access Denied.

The XML configuration:

  ...  

And the Java configuration:

@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedPage("/my-error-page"); }

When users try to access a resource without having enough authorities, they will be redirected to “/my-error-page”.

6.2. Custom AccessDeniedHandler

Next, let's see how to write our custom AccessDeniedHandler:

@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException, ServletException { response.sendRedirect("/my-error-page"); } }

And now let's configure it using XML configuration:

  ...  

0r using Java configuration:

@Autowired private CustomAccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler) }

Note how in our CustomAccessDeniedHandler, we can customize the response as we wish by redirecting or displaying a custom error message.

6.3. REST and Method-Level Security

Finally, let's see how to handle method-level security @PreAuthorize, @PostAuthorize, and @Secure Access Denied.

Of course, we'll use the global exception handling mechanism that we discussed earlier to handle the AccessDeniedException as well:

@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ AccessDeniedException.class }) public ResponseEntity handleAccessDeniedException( Exception ex, WebRequest request) { return new ResponseEntity( "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN); } ... }

7. Spring Boot Support

Spring Boot provides an ErrorController implementation to handle errors in a sensible way.

In a nutshell, it serves a fallback error page for browsers (a.k.a. the Whitelabel Error Page) and a JSON response for RESTful, non-HTML requests:

{ "timestamp": "2019-01-17T16:12:45.977+0000", "status": 500, "error": "Internal Server Error", "message": "Error processing the request!", "path": "/my-endpoint-with-exceptions" }

As usual, Spring Boot allows configuring these features with properties:

  • server.error.whitelabel.enabled: can be used to disable the Whitelabel Error Page and rely on the servlet container to provide an HTML error message
  • server.error.include-stacktrace: with an always value; includes the stacktrace in both the HTML and the JSON default response

Apart from these properties, we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

We can also customize the attributes that we want to show in the response by including an ErrorAttributes bean in the context. We can extend the DefaultErrorAttributes class provided by Spring Boot to make things easier:

@Component public class MyCustomErrorAttributes extends DefaultErrorAttributes { @Override public Map getErrorAttributes( WebRequest webRequest, boolean includeStackTrace) { Map errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); errorAttributes.put("locale", webRequest.getLocale() .toString()); errorAttributes.remove("error"); //... return errorAttributes; } }

If we want to go further and define (or override) how the application will handle errors for a particular content type, we can register an ErrorController bean.

Again, we can make use of the default BasicErrorController provided by Spring Boot to help us out.

Ad esempio, immagina di voler personalizzare il modo in cui la nostra applicazione gestisce gli errori attivati ​​negli endpoint XML. Tutto quello che dobbiamo fare è definire un metodo pubblico usando @RequestMapping e affermando che produce il tipo di supporto application / xml :

@Component public class MyErrorController extends BasicErrorController { public MyErrorController(ErrorAttributes errorAttributes) { super(errorAttributes, new ErrorProperties()); } @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE) public ResponseEntity xmlError(HttpServletRequest request) { // ... } }

8. Conclusione

Questo articolo ha discusso diversi modi per implementare un meccanismo di gestione delle eccezioni per un'API REST in Spring, iniziando con il vecchio meccanismo e continuando con il supporto Spring 3.2 e in 4.xe 5.x.

Come sempre, il codice presentato in questo articolo è disponibile su GitHub.

Per il codice relativo a Spring Security, puoi controllare il modulo spring-security-rest.

REST fondo

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO