Teil 1: Exception Handling

Wie man ein zentralisiertes REST API Exception Handling mit BeanValidation kombiniert

Ulrich Cech

©Shutterstock/McIek

In dieser dreiteiligen Artikelserie soll eine Lösung vorgestellt werden, wie man ein zentralisiertes Exception Handling für eine REST API aufbauen und dieses zusätzlich mit BeanValidation koppeln bzw. ergänzen kann. Dazu sollen ausschließlich Java-EE-7-Standards (JAX-RS, JAXB für die Serialisierung in XML oder JSON, usw.) verwendet werden, weswegen keinerlei zusätzliche Libraries erforderlich sind.

In einer REST API ist das Exception Handling sehr wichtig. Zum einen wollen wir den Benutzer der API nicht mit einem nichtssagenden Stacktrace überfluten oder die Standard-Fehlerseite vom ApplicationServer präsentieren. Abgesehen davon ist es auch ein gewisses Sicherheitsrisiko, einen Stacktrace nach außen zu geben, da dadurch zahlreiche interne Implementierungsinformationen veröffentlicht werden, die einem potentiellen Angreifer das Leben deutlich vereinfachen.

Zum anderen wird die REST API auch von anderen Programmiersprachen als Java verwendet und die jeweiligen Entwickler können/wollen dann keinen Java-Stacktrace interpretieren müssen. Dafür wollen wir aussagekräftige und auch auswertbare Fehler liefern, die die Verwendung der REST API vereinfachen und damit auch zur Akzeptanz und höheren Nutzbarkeit der entsprechenden REST API führen. Dies bedeutet, dass wir korrekte und passende HTTP-Statuscodes wie auch eine einheitliche Fehlerdatenstruktur zurückliefern wollen. Dadurch können die Ergebnisse auch maschinell verarbeitet werden, beispielsweise bei Statusabfragen.

Zentralisiertes Exception Handling

Als erstes müssen wir untersuchen, wo in unserem Code überall Exceptions auftreten können. Potentiell kann dies selbstverständlich überall passieren, weil wir nicht nur die CheckedExceptions, sondern zusätzlich jegliche RuntimeExceptions berücksichtigen müssen, die auch in ThirdParty Libraries geworfen und in unserem Code durchschlagen können. Bei den CheckedExceptions werden wir schon vom Compiler auf die Behandlung aufmerksam gemacht; die RuntimeExceptions jedoch können wir quasi nur durch Analyse jeder Methode, selbst von ThirdParty Libraries herausfinden. Wir wollen natürlich jetzt nicht in jeder Methode oder zumindest in jeder REST-API-Endpoint-Methode die üblichen try/catch-Blöcke einbauen. Dies führt zu Boilerplate-Code und reduziert damit die Les- und Wartbarkeit. An folgendem Beispiel erkennt man dies sehr deutlich:


@RequestScoped
@Path("users")
public class UsersResource {

    @Inject
    UserRepository userRepository;

    @POST
    public Response registerUser(User userRequest) {
        // try {
            User managedUser = userRepository.register(userRequest);
            URI uri = super.info.getAbsolutePathBuilder()
                    .path("/" + managedUser.getEmail()).build();
            return Response.created(uri).entity(managedUser).build();
        // } catch (Exception ex) {
        //     if (ex instanceof ExceptionA) {
        //         return <X>;
        //     }
        //     if (ex instanceof ExceptionB) {
        //         return <Y>;
        //     }
        //     usw.
        // }
    }
}

Den auskommentierten Code müssten wir in jeder Methode implementieren, wodurch wir auch das DRY-Prinzip (Don’t repeat yourself) verletzen würden, was wir als gute Entwickler selbstverständlich nicht wollen. Selbst, wenn es sich nur um einen einzigen catch-Block handelt bzw. dort nur eine einzige Fehlersituation abgefangen werden soll, umfasst der Exception-Handling-Code mehr Zeilen als der funktionale Code, der in diesem Beispiel lediglich 3 Zeilen umfasst. Glücklicherweise bietet JavaEE an dieser Stelle bereits geeignete Hausmittelchen an, um das ExceptionHandling an zentraler Stelle zu behandeln:

  • Interceptoren
  • ExceptionMapper

Ein zentraler, allerdings minimalistischer ExceptionInterceptor, der einen HTTP-Statuscode 500 (Internal Server Error) sowie die textuelle Fehlermeldung der Exception liefert, könnte folgende Struktur haben:

public class APIExceptionInterceptor {

@AroundInvoke
public Object handleException(InvocationContext context) { Object proceedResponse;
try {
proceedResponse = context.proceed();
} catch (Exception ex) {
return Response.serverError().entity(ex.getMessage()).build();
}
return proceedResponse;
}
}

Der Interceptor wird in allen REST-Resourcenklassen über die @Interceptor-Annotation auf Klassenebene aktiviert. Beim Aufruf einer REST-Methode wird zuerst die handleException()-Methode des Interceptors gerufen, die dann wiederum über die Zeile proceedResponse = context.proceed(); den eigentlichen Code der REST-Methoden ausführt. Wird nun während dieser Ausführung eine Exception geworfen, so wird diese vom catch-Block der handleException()-Methode verarbeitet.

@RequestScoped
@Path("users")
@Interceptor(APIExceptionInterceptor.class)  // <---
public class UsersResource {
   // ...
}

Anmerkung: Werden mehrere Interceptoren benötigt, können über die Annotation @Interceptors({APIExceptionInterceptor.class, B.class, C.class}) mehrere Klassen angegeben werden. Außerdem können Interceptoren auch auf Methodenebene angewendet werden, um eine feingranulare Steuerung zu erhalten. Exceptions wollen wir aber generell verarbeiten, daher muss der Interceptor auf Klassenebene angegeben werden. Außerdem sollte der Interceptor in diesem Fall als erstes in der Auflistung aufgeführt werden, damit Exceptions aus den anderen Interceptoren ebenfalls vom API ExceptionInterceptor verarbeitet werden können.

Schließlich können wir die Interceptoren noch eleganter über NameBindung/InterceptorBindung definieren. (http://docs.oracle.com/javaee/7/api/javax/ws/rs/NameBinding.html, http://docs.oracle.com/javaee/7/api/javax/interceptor/InterceptorBinding.html)

Dieser minimalistische Ansatz liefert uns jetzt bei jeder Exception einen „Internal Server Error“ mit der textuellen Fehlermeldung der tatsächlich aufgetretenen Exception. Diese Information hilft nur bedingt, aber wir haben jetzt immerhin schon einmal eine zentralisiertere Lösung ohne Stacktrace. Serverseitig müssen wir uns aber noch mit anderen Exceptions befassen, die nicht vom ExceptionInterceptor behandelt werden können. Dies sind beispielsweise Exceptions vom JAX RS Framework, die geworfen werden, bevor der HTTP Request überhaupt an der REST-Ressourcenklasse ankommt. Hierzu gehören beispielsweise folgende Exceptions:

  • javax.ws.rs.NotFoundException
  • javax.ws.rs.NotAcceptableException
  • javax.ws.rs.NotSupportedException

Im javax.ws.rs-package befinden sich noch weitere Exceptions, die man auf dem gleichen Weg behandeln sollte. Diese lassen wir der Übersichtlichkeit halber aber außen vor.

Um die Exceptions abzufangen und analog zu unserem API ExceptionInterceptor (siehe Codebespiel 2) einen HTTP-Statuscode sowie den Fehlermeldungstext der Exception zurückzuliefern, bedienen wir uns der javax.ws.rs.ext.ExceptionMapper. Dieses generische Interface kann exemplarisch folgendermaßen implementiert werden:

import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class JAXRSNotFoundMapper implements
                                 ExceptionMapper<NotFoundException> {

    @Override
    public Response toResponse(NotFoundException ex) {
        return Response.status(Response.Status.NOT_FOUND)
                .entity(ex.getMessage()).build();
    }
}

import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class JAXRSNotAcceptableMapper
                    implements ExceptionMapper<NotAcceptableException> {

    @Override
    public Response toResponse(NotAcceptableException ex) {
        return Response.status(Response.Status.NOT_ACCEPTABLE)
                .entity(ex.getMessage()).build();
    }
}

Die weiteren Exceptions (z.B. javax.ws.rs.NotSupportedException) können durch analoge ExceptionMapper behandelt werden. Über die Annotation @Provider werden diese Mapper beim Starten der Web-Applikation automatisch registriert, es sind also keinerlei weitere Registrierungseinträge in der web.xml notwendig.

Anmerkung: Diese ExceptionMapper greifen NICHT pauschal in der Web-Applikation. Sie greifen nur für URIs, die sich unterhalb des JAX RS Base Path befinden. Dieser Base Path wird in einer JAX-RS-Applikation bekanntermaßen hier definiert:

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/api")
public class JAXRSConfiguration extends Application {
}

Das heißt konkret, dass die ExceptionMapper bei allen URIs unterhalb von /api/ greifen. In unserem Beispiel haben wir bisher nur eine UsersResource mit folgendem Path definiert:

http://<server>:<port>/<context>/api/users

Würden wir jetzt beispielsweise die Resource http://<server>:<port>/<context>/api/accounts abfragen wollen (die nicht existiert), würde JAX RS eine NotFoundException erzeugen, die wiederum von unserem Mapper abgefangen und verarbeitet wird.

 Zwischenergebnis

Wir können nun die Exceptions, die unser Applikationscode erzeugt, über den ExceptionInterceptor abfangen, und wir können über die ExceptionMapper diejenigen Exceptions behandeln, die das JAX RS Framework generiert. Der Interceptor sowie die Mapper liefern uns ein einheitliches Ergebnis (HTTP-Statuscode und Fehlertext der Exception).

Morgen geht`s weiter: Im nächsten Teil der Serie wenden wir uns der Frage zu, wie sich die Exceptions in eine einheitliche Antwortstruktur umwandeln lassen, bevor wir schließlich die Integration von BeanValidation angehen.

Aufmacherbild: Software developer programming code von Shutterstock / Urheberrecht: McIek

Verwandte Themen:

Geschrieben von
Ulrich Cech
Ulrich Cech
Ulrich Cech ist Senior Developer bei der dreamIT GmbH in Hamburg. Er verfügt über langjährige Erfahrung als Entwickler im Java-Enterprise-Bereich, insbesondere Java EE, und beschäftigt sich aktuell mit Java-EE-7-Migrationen. Sein besonderes Interesse liegt zusätzlich in leichtgewichtigen Server-Architekturen auf REST-Basis.
Kommentare

Hinterlasse einen Kommentar

3 Kommentare auf "Wie man ein zentralisiertes REST API Exception Handling mit BeanValidation kombiniert"

avatar
4000
  Subscribe  
Benachrichtige mich zu:
Harald Wellmann
Gast

Wozu der Interzeptor? Warum nicht einfach auch andere Exceptions über einen ExceptionMapper abfangen?

Übrigens fehlt das Typargument: ExceptionMapper.

Ulrich Cech
Gast

Hallo Harald,

erst einmal vielen Dank für den Hinweis mit den fehlenden Typargumenten. Diese fehlen tatsächlich (obwohl sie im eingereichten Text vorhanden waren)… dies wird gerade von der Redaktion korrigiert.

Man könnte auch über einen zentralen ExceptionMapper gehen, allerdings bieten Interceptoren eine etwas höhere Flexibilität und man vermeidet „if/else-Konstrukte“ im Mapper. Bei kleineren Projekten würde aber sicherlich auch ein ExceptionMapper funktioneren.

Kypriani Sinaris
Mitglied

Hallo!
Vielen Dank für den Hinweis! Wir haben entsprechende Code-Passagen korrigiert.