Fehlerbehandlung für REST mit Feder

REST Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Übersicht

In diesem Lernprogramm wird veranschaulicht, wie die Ausnahmebehandlung mit Spring für eine REST-API implementiert wird. Wir werden auch einen kleinen historischen Überblick bekommen und sehen, welche neuen Optionen die verschiedenen Versionen eingeführt haben.

Vor Spring 3.2 waren die beiden Hauptansätze für die Behandlung von Ausnahmen in einer Spring MVC-Anwendung HandlerExceptionResolver oder die Annotation @ExceptionHandler . Beide haben einige klare Nachteile.

Seit 3.2 haben wir die Annotation @ControllerAdvice , um die Einschränkungen der beiden vorherigen Lösungen zu beheben und eine einheitliche Ausnahmebehandlung für eine gesamte Anwendung zu fördern.

Jetzt führt Spring 5 die ResponseStatusException- Klasse ein - eine schnelle Möglichkeit zur grundlegenden Fehlerbehandlung in unseren REST-APIs.

All dies haben eines gemeinsam: Sie gehen sehr gut mit der Trennung von Bedenken um . Die App kann normalerweise Ausnahmen auslösen, um auf einen Fehler hinzuweisen, der dann separat behandelt wird.

Schließlich werden wir sehen, was Spring Boot auf den Tisch bringt und wie wir es so konfigurieren können, dass es unseren Anforderungen entspricht.

2. Lösung 1: der Controller-Level @ExceptionHandler

Die erste Lösung funktioniert auf @ Controller- Ebene. Wir werden eine Methode definieren, um Ausnahmen zu behandeln und diese mit @ExceptionHandler zu kommentieren :

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

Dieser Ansatz hat einen großen Nachteil: T er @ExceptionHandler kommentierte Methode ist nur dann aktiv für diesen speziellen Regler , nicht global für die gesamte Anwendung. Wenn Sie dies zu jedem Controller hinzufügen, eignet es sich natürlich nicht für einen allgemeinen Ausnahmebehandlungsmechanismus.

Wir können diese Einschränkung umgehen, indem alle Controller eine Basis-Controller-Klasse erweitern.

Diese Lösung kann jedoch ein Problem für Anwendungen sein, bei denen dies aus irgendeinem Grund nicht möglich ist. Beispielsweise können sich die Controller bereits von einer anderen Basisklasse erstrecken, die sich möglicherweise in einem anderen Glas befindet oder nicht direkt modifizierbar ist oder selbst nicht direkt modifizierbar ist.

Als Nächstes sehen wir uns einen anderen Weg an, um das Problem der Ausnahmebehandlung zu lösen - einen, der global ist und keine Änderungen an vorhandenen Artefakten wie Controllern enthält.

3. Lösung 2: der HandlerExceptionResolver

Die zweite Lösung besteht darin, einen HandlerExceptionResolver zu definieren . Dadurch werden alle von der Anwendung ausgelösten Ausnahmen behoben. Außerdem können wir einen einheitlichen Mechanismus zur Ausnahmebehandlung in unserer REST-API implementieren .

Bevor wir uns für einen benutzerdefinierten Resolver entscheiden, gehen wir die vorhandenen Implementierungen durch.

3.1. ExceptionHandlerExceptionResolver

Dieser Resolver wurde in Spring 3.1 eingeführt und ist im DispatcherServlet standardmäßig aktiviert . Dies ist eigentlich die Kernkomponente der Funktionsweise des zuvor vorgestellten @ ExceptionHandler- Mechanismus.

3.2. DefaultHandlerExceptionResolver

Dieser Resolver wurde in Spring 3.0 eingeführt und ist im DispatcherServlet standardmäßig aktiviert .

Es wird verwendet, um Standard-Spring-Ausnahmen für die entsprechenden HTTP- Statuscodes aufzulösen, nämlich Clientfehler 4xx und Serverfehler 5xx . Hier ist die vollständige Liste der behandelten Spring Exceptions und deren Zuordnung zu Statuscodes.

Während der Statuscode der Antwort richtig eingestellt wird, besteht eine Einschränkung darin, dass nichts auf den Hauptteil der Antwort festgelegt wird. Und für eine REST-API - der Statuscode reicht nicht aus, um dem Client angezeigt zu werden - muss die Antwort auch einen Text enthalten, damit die Anwendung zusätzliche Informationen über den Fehler bereitstellen kann.

Dies kann durch Konfigurieren der Ansichtsauflösung und Rendern von Fehlerinhalten über ModelAndView behoben werden. Die Lösung ist jedoch eindeutig nicht optimal. Aus diesem Grund hat Spring 3.2 eine bessere Option eingeführt, die wir in einem späteren Abschnitt behandeln werden.

3.3. ResponseStatusExceptionResolver

Dieser Resolver wurde ebenfalls in Spring 3.0 eingeführt und ist standardmäßig im DispatcherServlet aktiviert .

Die Hauptverantwortung besteht darin, die für benutzerdefinierte Ausnahmen verfügbare @ ResponseStatus- Annotation zu verwenden und diese Ausnahmen HTTP-Statuscodes zuzuordnen.

Eine solche benutzerdefinierte Ausnahme kann folgendermaßen aussehen:

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

Dieser Resolver ist wie der DefaultHandlerExceptionResolver in der Art und Weise, wie er mit dem Hauptteil der Antwort umgeht , eingeschränkt. Er ordnet den Statuscode der Antwort zu, der Hauptteil ist jedoch weiterhin null.

3.4. SimpleMappingExceptionResolver und AnnotationMethodHandlerExceptionResolver

Der SimpleMappingExceptionResolver gibt es schon seit geraumer Zeit. Es stammt aus dem älteren Spring MVC-Modell und ist für einen REST-Service nicht sehr relevant. Wir verwenden es grundsätzlich, um Ausnahmeklassennamen zuzuordnen, um Namen anzuzeigen.

Der AnnotationMethodHandlerExceptionResolver wurde in Spring 3.0 eingeführt, um Ausnahmen über die Annotation @ExceptionHandler zu behandeln , wurde jedoch von ExceptionHandlerExceptionResolver ab Spring 3.2 nicht mehr unterstützt.

3.5. Benutzerdefinierter HandlerExceptionResolver

Die Kombination von DefaultHandlerExceptionResolver und ResponseStatusExceptionResolver trägt wesentlich dazu bei, einen guten Mechanismus zur Fehlerbehandlung für einen Spring RESTful-Service bereitzustellen. Der Nachteil ist, wie bereits erwähnt, keine Kontrolle über den Körper der Reaktion.

Idealerweise möchten wir entweder JSON oder XML ausgeben können, je nachdem, welches Format der Client angefordert hat (über den Accept- Header).

Dies allein rechtfertigt die Erstellung eines neuen, benutzerdefinierten Auslösers für Ausnahmen :

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

Ein Detail, das hier zu beachten ist, ist, dass wir Zugriff auf die Anforderung selbst haben, sodass wir den Wert des vom Client gesendeten Accept- Headers berücksichtigen können .

Wenn der Client beispielsweise nach application / json fragt , möchten wir im Falle einer Fehlerbedingung sicherstellen, dass wir einen mit application / json codierten Antworttext zurückgeben .

Das andere wichtige Implementierungsdetail ist, dass wir ein ModelAndView zurückgeben - dies ist der Hauptteil der Antwort , und es ermöglicht uns, alles Notwendige darauf festzulegen.

Dieser Ansatz ist ein konsistenter und leicht konfigurierbarer Mechanismus für die Fehlerbehandlung eines Spring REST-Service.

Es gibt jedoch Einschränkungen: Es interagiert mit der einfachen HtttpServletResponse und passt in das alte MVC-Modell, das ModelAndView verwendet , sodass noch Verbesserungspotenzial besteht.

4. Lösung 3: @ControllerAdvice

Spring 3.2 bietet Unterstützung für einen globalen @ExceptionHandler mit der Annotation @ControllerAdvice .

Dies ermöglicht einen Mechanismus, der sich vom älteren MVC-Modell löst und ResponseEntity zusammen mit der Typensicherheit und Flexibilität von @ExceptionHandler verwendet :

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

Stellen Sie sich zum Beispiel vor, wir möchten anpassen, wie unsere Anwendung mit in XML-Endpunkten ausgelösten Fehlern umgeht. Alles, was wir tun müssen, ist, eine öffentliche Methode mithilfe von @RequestMapping zu definieren und anzugeben , dass sie den Medientyp application / xml erzeugt :

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

In diesem Artikel wurden verschiedene Möglichkeiten zum Implementieren eines Ausnahmebehandlungsmechanismus für eine REST-API in Spring erläutert, beginnend mit dem älteren Mechanismus und fortlaufend mit der Spring 3.2-Unterstützung bis hin zu 4.x und 5.x.

Wie immer ist der in diesem Artikel vorgestellte Code auf GitHub verfügbar.

Für den Spring Security-bezogenen Code können Sie das Spring-Security-Rest-Modul überprüfen.

REST unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs