Fehlerbehandlung für REST mit Spring

Fehlerbehandlung für REST mit Feder

1. Überblick

Dieser Artikel zeigthow to implement Exception Handling with Spring for a REST API. Wir erhalten auch einen historischen Überblick und sehen, welche neuen Optionen die verschiedenen Versionen eingeführt haben.

Before Spring 3.2, the two main approaches to handling exceptions in a Spring MVC application were: HandlerExceptionResolver or the @ExceptionHandler annotation. Beide haben einige klare Nachteile.

Since 3.2 we’ve had the @ControllerAdvice annotation, um die Einschränkungen der beiden vorherigen Lösungen zu beseitigen und eine einheitliche Ausnahmebehandlung für eine gesamte Anwendung zu fördern.

JetztSpring 5 introduces the ResponseStatusException class: Ein schneller Weg zur grundlegenden Fehlerbehandlung in unseren REST-APIs.

Alle diese haben eines gemeinsam: Sie gehen sehr gut mit denseparation of concernsum. Die App kann normalerweise Ausnahmen auslösen, um einen Fehler anzuzeigen - Ausnahmen, die dann separat behandelt werden.

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.

Weitere Lektüre:

Benutzerdefinierte Behandlung von Fehlernachrichten für die REST-API

Implementieren Sie einen globalen Ausnahmehandler für eine REST-API mit Spring.

Read more

Leitfaden für Spring Data REST-Validatoren

Schneller und praktischer Leitfaden für Spring Data REST-Validatoren

Read more

Spring MVC Custom Validation

Erfahren Sie, wie Sie eine benutzerdefinierte Validierungsanmerkung erstellen und in Spring MVC verwenden.

Read more

2. Lösung 1 - Der Reglerpegel@ExceptionHandler

Die erste Lösung funktioniert auf der Ebene von@Controller- wir definieren eine Methode zur Behandlung von Ausnahmen und kommentieren diese mit@ExceptionHandler:

public class FooController{

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

Dieser Ansatz hat einen großen Nachteil -the @ExceptionHandler annotated method is only active for that particular Controller, nicht global für die gesamte Anwendung. Wenn Sie dies jedem Controller hinzufügen, ist es natürlich nicht für einen allgemeinen Ausnahmebehandlungsmechanismus geeignet.

Wir können diese Einschränkung umgehen, indem wirall Controllers extend a Base Controller class haben. Dies kann jedoch ein Problem für Anwendungen sein, bei denen dies aus irgendeinem Grund nicht möglich ist. Beispielsweise können die Controller bereits von einer anderen Basisklasse stammen, die sich in einem anderen Jar befindet oder nicht direkt modifizierbar ist, oder sie können selbst nicht direkt modifizierbar sein.

Als Nächstes werden wir uns einen anderen Weg ansehen, 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 -The HandlerExceptionResolver

Die zweite Lösung besteht darin, einHandlerExceptionResolver zu definieren. Dadurch werden alle von der Anwendung ausgelösten Ausnahmen behoben. Außerdem können wiruniform exception handling mechanism in unserer REST-API implementieren.

Bevor Sie sich 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 standardmäßig inDispatcherServlet 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 standardmäßig inDispatcherServlet aktiviert. Es wird verwendet, um Standard-Spring-Ausnahmen für die entsprechenden HTTP-Statuscodes aufzulösen, nämlich Client-Fehler -4xxund Serverfehler -5xx. Here’s the full list der behandelten Spring Exceptions und wie sie Statuscodes zugeordnet werden.

Während der Statuscode der Antwort richtig eingestellt ist, beträgt einlimitation is that it doesn’t set anything to the body of the Response. Und für eine REST-API - der Statuscode ist wirklich nicht genug Information, um dem Client präsentiert zu werden - muss die Antwort auch einen Body haben, damit die Anwendung zusätzliche Informationen über den Fehler geben kann.

Dies kann gelöst werden, indem die Ansichtsauflösung konfiguriert und der Fehlerinhalt überModelAndView gerendert wird. 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 inDispatcherServlet aktiviert. Die Hauptverantwortung besteht darin, die für benutzerdefinierte Ausnahmen verfügbare Annotation@ResponseStatuszu 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);
    }
}

Wie dieDefaultHandlerExceptionResolver ist dieser Resolver in der Art und Weise, wie er mit dem Hauptteil der Antwort umgeht, begrenzt. Er ordnet den Statuscode der Antwort zu, aber der Hauptteil ist immer nochnull.

3.4. SimpleMappingExceptionResolver und AnnotationMethodHandlerExceptionResolver

DieSimpleMappingExceptionResolver gibt es schon seit einiger Zeit - sie stammen aus dem älteren Spring MVC-Modell und sindnot very relevant for a REST Service. Grundsätzlich wird es verwendet, um Ausnahmeklassennamen zuzuordnen, um Namen anzuzeigen.

AnnotationMethodHandlerExceptionResolver wurde in Frühjahr 3.0 eingeführt, um Ausnahmen durch die Annotation@ExceptionHandler zu behandeln, wurde jedoch ab Frühjahr 3.2 umExceptionHandlerExceptionResolver verworfen.

3.5. BenutzerdefinierteHandlerExceptionResolver

Die Kombination vonDefaultHandlerExceptionResolver undResponseStatusExceptionResolver trägt wesentlich dazu bei, einen guten Fehlerbehandlungsmechanismus für einen Spring RESTful Service bereitzustellen. Der Nachteil ist - wie bereits erwähnt -no control over the body of the response.

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

Dies allein rechtfertigt die Erstellung vona new, custom exception resolver:

@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 dierequest selbst haben, sodass wir den Wert des vom Client gesendetenAccept-Headers berücksichtigen können.

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

Das andere wichtige Implementierungsdetail istwe return a ModelAndView – this is the body of the response und ermöglicht es uns, alles festzulegen, was darauf erforderlich ist.

Dieser Ansatz ist ein konsistenter und leicht konfigurierbarer Mechanismus für die Fehlerbehandlung eines Spring-REST-Service. Es gibt jedoch Einschränkungen: Es interagiert mitHtttpServletResponse auf niedriger Ebene und passt in das alte MVC-Modell, dasModelAndView verwendet. Es gibt also noch Raum für Verbesserungen.

4. Lösung 3 -@ControllerAdvice

Feder 3.2 unterstützta global @ExceptionHandler with the @ControllerAdvice annotation. Dies ermöglicht einen Mechanismus, der sich vom älteren MVC-Modell löst undResponseEntity 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);
    }
}


Die Annotation@ControllerAdvice ermöglicht unsconsolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

Der eigentliche Mechanismus ist äußerst einfach, aber auch sehr flexibel. Es gibt uns:

  • Volle Kontrolle über den Inhalt der Antwort sowie den Statuscode

  • Abbildung mehrerer Ausnahmen auf dieselbe Methode, die zusammen verarbeitet werden sollen, und

  • Es nutzt die Antwort des neueren RESTfulResposeEntity__

Eine Sache, die Sie hier beachten sollten, istmatch the exceptions declared with @ExceptionHandler with the exception used as the argument of the method. Wenn diese nicht übereinstimmen, wird sich der Compiler nicht beschweren - kein Grund, und Spring wird sich auch nicht beschweren.

Wenn die Ausnahme jedoch tatsächlich zur Laufzeit ausgelöst wird, wirdthe exception resolving mechanism will fail with:

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

5. Lösung 4 -ResponseStatusException (Feder 5 und höher)

In Frühjahr 5 wurde die KlasseResponseStatusExceptioneingeführt. Wir können eine Instanz davon erstellen, die einHttpStatus und optional einreason und eincause bereitstellt:

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

Was sind die Vorteile der Verwendung vonResponseStatusException?

  • Hervorragend geeignet für das Prototyping: Wir können recht schnell eine Basislösung implementieren

  • Ein Typ, mehrere Statuscodes: Ein Ausnahmetyp kann zu mehreren unterschiedlichen Antworten führen. This reduces tight coupling compared to the @ExceptionHandler

  • Wir müssen nicht so viele benutzerdefinierte Ausnahmeklassen erstellen

  • More control over exception handling, da die Ausnahmen programmgesteuert erstellt werden können

Und was ist mit den Kompromissen?

  • Es gibt keine einheitliche Methode zur Ausnahmebehandlung: Es ist schwieriger, einige anwendungsweite Konventionen durchzusetzen, als@ControllerAdvice, die einen globalen Ansatz bieten

  • Code-Duplikation: Möglicherweise replizieren wir Code auf mehreren Controllern

Wir sollten auch beachten, dass es möglich ist, verschiedene Ansätze in einer Anwendung zu kombinieren.

For example, we can implement a @ControllerAdvice globally, but also ResponseStatusExceptions locally. Wir müssen jedoch vorsichtig sein: Wenn dieselbe Ausnahme auf mehrere Arten behandelt werden kann, stellen wir möglicherweise ein überraschendes Verhalten fest. Eine mögliche Konvention besteht darin, eine bestimmte Art von Ausnahme immer auf eine Weise zu behandeln.

Weitere Details und Beispiele finden Sie in unserentutorial on ResponseStatusException.

6. Behandeln Sie den in Spring Security verweigerten Zugriff

Der Zugriff verweigert tritt auf, wenn ein authentifizierter Benutzer versucht, auf Ressourcen zuzugreifen, für die er nicht über ausreichende Berechtigungen verfügt.

6.1. MVC - Benutzerdefinierte Fehlerseite

Schauen wir uns zunächst den MVC-Stil der Lösung an und sehen, wie eine Fehlerseite für Zugriff verweigert angepasst wird:

Die XML-Konfiguration:


    
    ...
    

Und die Java-Konfiguration:

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

Wenn Benutzer versuchen, auf eine Ressource zuzugreifen, ohne über ausreichende Berechtigungen zu verfügen, werden sie zu „/my-error-page“ umgeleitet.

6.2. BenutzerdefinierteAccessDeniedHandler

Als nächstes wollen wir sehen, wie wir unsere benutzerdefiniertenAccessDeniedHandler schreiben:

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

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

Und jetzt konfigurieren wir es mitXML Configuration:


    
    ...
    

Oder mit Java-Konfiguration:

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

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

Beachten Sie, wie wir in unserenCustomAccessDeniedHandler die Antwort nach Belieben anpassen können, indem wir eine benutzerdefinierte Fehlermeldung umleiten oder anzeigen.

6.3. Sicherheit auf REST- und Methodenebene

Lassen Sie uns abschließend sehen, wie die Sicherheit auf Methodenebene@PreAuthorize,@PostAuthorize und@Secure Zugriff verweigert wird.

Wir werden natürlich den zuvor diskutierten globalen Ausnahmebehandlungsmechanismus verwenden, um auch dieAccessDeniedExceptionzu behandeln:

@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-Unterstützung

Spring Boot bietet eineErrorController-Implementierung, um Fehler auf sinnvolle Weise zu behandeln.

Kurz gesagt, es wird eine Fallback-Fehlerseite für Browser (auch als Whitelabel-Fehlerseite bezeichnet) und eine JSON-Antwort für RESTful-Anfragen (ohne HTML) bereitgestellt:

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

In Spring Boot können Sie wie gewohnt die folgenden Funktionen mit Eigenschaften konfigurieren:

  • server.error.whitelabel.enabled: kann verwendet werden, um die Whitelabel-Fehlerseite zu deaktivieren und sich auf den Servlet-Container zu verlassen, um eine HTML-Fehlermeldung bereitzustellen

  • server.error.include-stacktrace: mit einemalways -Wert enthält die Stacktrace sowohl in der HTML- als auch in der JSON-Standardantwort

Abgesehen von diesen Eigenschaften sindwe can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

Wir können auch die Attribute anpassen, die in der Antwort angezeigt werden sollen, indem wir eineErrorAttributes bean in den Kontext aufnehmen. Wir können die von Spring Boot bereitgestellteDefaultErrorAttributes-Klasse erweitern, um die Sache zu vereinfachen:

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

Wenn wir weiter gehen und definieren (oder überschreiben) möchten, wie die Anwendung Fehler für einen bestimmten Inhaltstyp behandelt, können wir eineErrorController -Sbean registrieren.

Auch hier können wir den von Spring Boot bereitgestellten StandardBasicErrorController verwenden, um uns zu helfen.

Stellen Sie sich zum Beispiel vor, wir möchten anpassen, wie unsere Anwendung mit Fehlern umgeht, die in XML-Endpunkten ausgelöst werden. Alles, was wir tun müssen, ist, eine öffentliche Methode unter Verwendung von@RequestMapping zu definieren und anzugeben, dass sie den Medientypapplication/xmlerzeugt:

@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 Lernprogramm wurden verschiedene Möglichkeiten zum Implementieren eines Ausnahmebehandlungsmechanismus für eine REST-API im Frühjahr erläutert. Begonnen wurde mit dem älteren Mechanismus und mit der Unterstützung für Spring 3.2 sowie mit 4.x und 5.x.

Wie immer ist der in diesem Artikel vorgestellte Codeover on Github verfügbar.

Für den Code für Spring Security können Sie das Modulspring-security-restüberprüfen.