JAX-RS / Jersey Wie kann man die Fehlerbehandlung anpassen?

216

Ich lerne JAX-RS (auch bekannt als JSR-311) mit Jersey. Ich habe erfolgreich eine Root-Ressource erstellt und spiele mit Parametern herum:

@Path("/hello")
public class HelloWorldResource {

    @GET
    @Produces("text/html")
    public String get(
        @QueryParam("name") String name,
        @QueryParam("birthDate") Date birthDate) {

         // Return a greeting with the name and age
    }
}

Dies funktioniert hervorragend und verarbeitet alle Formate im aktuellen Gebietsschema, die vom Konstruktor Date (String) verstanden werden (z. B. JJJJ / MM / TT und MM / TT / JJJJ). Wenn ich jedoch einen Wert gebe, der ungültig ist oder nicht verstanden wird, erhalte ich eine Antwort 404.

Beispielsweise:

GET /hello?name=Mark&birthDate=X

404 Not Found

Wie kann ich dieses Verhalten anpassen? Vielleicht ein anderer Antwortcode (wahrscheinlich "400 Bad Request")? Was ist mit der Protokollierung eines Fehlers? Fügen Sie möglicherweise eine Beschreibung des Problems ("schlechtes Datumsformat") in einen benutzerdefinierten Header ein, um die Fehlerbehebung zu erleichtern. Oder eine vollständige Fehlerantwort mit Details und einem 5xx-Statuscode zurückgeben?

Mark Renouf
quelle

Antworten:

271

Es gibt verschiedene Ansätze, um das Fehlerbehandlungsverhalten mit JAX-RS anzupassen. Hier sind drei der einfacheren Möglichkeiten.

Der erste Ansatz besteht darin, eine Exception-Klasse zu erstellen, die die WebApplicationException erweitert.

Beispiel:

public class NotAuthorizedException extends WebApplicationException {
     public NotAuthorizedException(String message) {
         super(Response.status(Response.Status.UNAUTHORIZED)
             .entity(message).type(MediaType.TEXT_PLAIN).build());
     }
}

Und um diese neu erstellte Ausnahme zu werfen, müssen Sie einfach:

@Path("accounts/{accountId}/")
    public Item getItem(@PathParam("accountId") String accountId) {
       // An unauthorized user tries to enter
       throw new NotAuthorizedException("You Don't Have Permission");
}

Beachten Sie, dass Sie die Ausnahme nicht in einer Throws-Klausel deklarieren müssen, da WebApplicationException eine Laufzeitausnahme ist. Dies gibt eine 401-Antwort an den Client zurück.

Der zweite und einfachere Ansatz besteht darin, einfach eine Instanz von WebApplicationExceptiondirekt in Ihrem Code zu erstellen. Dieser Ansatz funktioniert, solange Sie keine eigenen Anwendungsausnahmen implementieren müssen.

Beispiel:

@Path("accounts/{accountId}/")
public Item getItem(@PathParam("accountId") String accountId) {
   // An unauthorized user tries to enter
   throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}

Auch dieser Code gibt eine 401 an den Client zurück.

Dies ist natürlich nur ein einfaches Beispiel. Sie können die Ausnahme bei Bedarf viel komplexer gestalten und den erforderlichen http-Antwortcode generieren.

Ein anderer Ansatz besteht darin, eine vorhandene Ausnahme, möglicherweise eine ObjectNotFoundException mit einer kleinen Wrapper-Klasse, die die ExceptionMappermit einer @ProviderAnmerkung versehene Schnittstelle implementiert . Dies teilt der JAX-RS-Laufzeit mit, dass der ausgelöste Antwortcode zurückgegeben wird, wenn die umschlossene Ausnahme ausgelöst wird ExceptionMapper.

Steven Levine
quelle
3
In Ihrem Beispiel sollte der Aufruf von super () etwas anders sein: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); Vielen Dank für den Einblick.
Jon Onstott
65
In dem in der Frage genannten Szenario haben Sie keine Möglichkeit, eine Ausnahme auszulösen, da Jersey eine Ausnahme auslöst, da keine Instanz des Date-Objekts aus dem Eingabewert erstellt werden kann. Gibt es eine Möglichkeit, die Jersey-Ausnahme abzufangen? Es gibt jedoch eine ExceptionMapper-Schnittstelle, die auch die von der Methode ausgelösten Ausnahmen abfängt (in diesem Fall get).
Rejeev Divakaran
7
Wie vermeiden Sie, dass die Ausnahme in Ihren Serverprotokollen angezeigt wird, wenn der 404 ein gültiger Fall und kein Fehler ist (dh jedes Mal, wenn Sie eine Ressource abfragen, um festzustellen, ob sie bereits vorhanden ist, wird bei Ihrem Ansatz eine Stapelverfolgung auf dem Server angezeigt Protokolle).
Guido
3
Erwähnenswert ist, dass Jersey 2.x Ausnahmen für einige der häufigsten HTTP-Fehlercodes definiert. Anstatt Ihre eigenen Unterklassen von WebApplication zu definieren, können Sie integrierte Unterklassen wie BadRequestException und NotAuthorizedException verwenden. Schauen Sie sich zum Beispiel die Unterklassen von javax.ws.rs.ClientErrorException an. Beachten Sie auch, dass Sie den Konstruktoren eine Detailzeichenfolge bereitstellen können. Beispiel: Neue BadRequestException auslösen ("Startdatum muss vor Enddatum liegen");
Bampfer
1
Sie haben vergessen, noch einen anderen Ansatz zu erwähnen: die Implementierung der ExceptionMapperSchnittstelle (die besser ist als die Erweiterung). Weitere Informationen finden Sie
ACV
70
@Provider
public class BadURIExceptionMapper implements ExceptionMapper<NotFoundException> {

public Response toResponse(NotFoundException exception){

    return Response.status(Response.Status.NOT_FOUND).
    entity(new ErrorResponse(exception.getClass().toString(),
                exception.getMessage()) ).
    build();
}
}

Erstellen Sie über Klasse. Dies behandelt 404 (NotFoundException) und hier in der toResponse-Methode können Sie Ihre benutzerdefinierte Antwort geben. Ebenso gibt es ParamException usw., die Sie zuordnen müssten, um benutzerdefinierte Antworten bereitzustellen.

Arnav
quelle
Sie können ExceptionMapper <Exception> auch für generische Ausnahmen verwenden
Saurabh
1
Dies würde auch WebApplicationExceptions behandeln, die vom JAX-RS-Client ausgelöst werden, und den Fehlerursprung verbergen. Besser eine benutzerdefinierte Ausnahme (nicht von WebApplicationException abgeleitet) oder WebApplications mit vollständiger Antwort auslösen. Vom JAX-RS-Client ausgelöste WebApplicationExceptions sollten direkt beim Aufruf behandelt werden. Andernfalls wird die Antwort eines anderen Dienstes als Antwort Ihres Dienstes weitergeleitet, obwohl es sich um einen nicht behandelten internen Serverfehler handelt.
Markus Kull
38

Jersey löst eine com.sun.jersey.api.ParamException aus, wenn die Parameter nicht freigegeben werden können. Eine Lösung besteht darin, einen ExceptionMapper zu erstellen, der diese Arten von Ausnahmen behandelt:

@Provider
public class ParamExceptionMapper implements ExceptionMapper<ParamException> {
    @Override
    public Response toResponse(ParamException exception) {
        return Response.status(Status.BAD_REQUEST).entity(exception.getParameterName() + " incorrect type").build();
    }
}
Jan Kronquist
quelle
Wo soll ich diesen Mapper speziell für Jersey erstellen, um ihn zu registrieren?
Patricio
1
Alles, was Sie tun müssen, ist, die @ Provider-Annotation hinzuzufügen. Weitere Informationen finden Sie hier: stackoverflow.com/questions/15185299/…
Jan Kronquist
27

Sie können auch eine wiederverwendbare Klasse für mit QueryParam annotierte Variablen schreiben

public class DateParam {
  private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

  private Calendar date;

  public DateParam(String in) throws WebApplicationException {
    try {
      date = Calendar.getInstance();
      date.setTime(format.parse(in));
    }
    catch (ParseException exception) {
      throw new WebApplicationException(400);
    }
  }
  public Calendar getDate() {
    return date;
  }
  public String format() {
    return format.format(value.getTime());
  }
}

dann benutze es so:

private @QueryParam("from") DateParam startDateParam;
private @QueryParam("to") DateParam endDateParam;
// ...
startDateParam.getDate();

Obwohl die Fehlerbehandlung in diesem Fall trivial ist (eine 400-Antwort wird ausgelöst), können Sie mit dieser Klasse die Parameterbehandlung im Allgemeinen herausrechnen, einschließlich Protokollierung usw.

Charlie Brooking
quelle
Ich versuche, einen benutzerdefinierten Abfrageparameter-Handler in Jersey hinzuzufügen (Migration von CXF). Dies sieht meiner Arbeit bemerkenswert ähnlich, aber ich weiß nicht, wie ich einen neuen Anbieter installieren / erstellen soll. Ihre obige Klasse zeigt mir das nicht. Ich verwende JodaTime DateTime-Objekte für QueryParam und habe keinen Anbieter, der sie dekodiert. Ist es genauso einfach, es zu unterordnen, ihm einen String-Konstruktor zu geben und damit umzugehen?
Christian Bongiorno
1
Erstellen Sie einfach eine Klasse wie die DateParamoben genannte, die ein org.joda.time.DateTimestatt einschließt java.util.Calendar. Sie verwenden das @QueryParameher mit als mit sich DateTimeselbst.
Charlie Brooking
1
Wenn Sie Joda DateTime verwenden, wird das Trikot mit DateTimeParam geliefert, das Sie direkt verwenden können. Sie müssen keine eigenen schreiben. Siehe github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Srikanth
Ich werde dies hinzufügen, weil es super nützlich ist, aber nur, wenn Sie Jackson mit Jersey verwenden. Jackson 2.x hat eine JodaModule, die mit der ObjectMapper registerModulesMethode registriert werden kann. Es kann alle Konvertierungen vom Typ Joda verarbeiten. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev
11

Eine naheliegende Lösung: Nehmen Sie einen String auf und konvertieren Sie ihn selbst in Date. Auf diese Weise können Sie das gewünschte Format definieren, Ausnahmen abfangen und den gesendeten Fehler entweder erneut auslösen oder anpassen. Für das Parsen sollte SimpleDateFormat einwandfrei funktionieren.

Ich bin mir sicher, dass es auch Möglichkeiten gibt, Handler für Datentypen zu verknüpfen, aber vielleicht ist in diesem Fall nur ein bisschen einfacher Code erforderlich.

StaxMan
quelle
7

Ich mag auch StaxMan auch, dass StaxMan dieses QueryParam wahrscheinlich als String implementieren und dann die Konvertierung durchführen und bei Bedarf erneut würde.

Wenn das länderspezifische Verhalten das gewünschte und erwartete Verhalten ist, würden Sie Folgendes verwenden, um den Fehler 400 BAD REQUEST zurückzugeben:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Weitere Optionen finden Sie im JavaDoc für javax.ws.rs.core.Response.Status .

dshaw
quelle
4

@QueryParam Dokumentation sagt

"Der Typ T des mit Anmerkungen versehenen Parameters, Felds oder der Eigenschaft muss entweder:

1) Seien Sie ein primitiver Typ.
2) Haben Sie einen Konstruktor, der ein einzelnes String-Argument akzeptiert.
3) Haben Sie eine statische Methode namens valueOf oder fromString, die ein einzelnes String-Argument akzeptiert (siehe zum Beispiel Integer.valueOf (String)).
4) Haben Sie a registrierte Implementierung von javax.ws.rs.ext.ParamConverterProvider JAX-RS-Erweiterungs-SPI, die eine javax.ws.rs.ext.ParamConverter-Instanz zurückgibt, die eine "from string" -Konvertierung für den Typ ausführen kann.
5) Sei List, Set oder SortedSet, wobei T 2, 3 oder 4 oben erfüllt. Die resultierende Sammlung ist schreibgeschützt. ""

Wenn Sie steuern möchten, welche Antwort an den Benutzer gesendet wird, wenn der Abfrageparameter in String-Form nicht in Ihren Typ T konvertiert werden kann, können Sie WebApplicationException auslösen. Dropwizard enthält die folgenden * Param-Klassen, die Sie für Ihre Anforderungen verwenden können.

BooleanParam, DateTimeParam, IntParam, LongParam, LocalDateParam, NonEmptyStringParam, UUIDParam. Sehen https://github.com/dropwizard/dropwizard/tree/master/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params

Wenn Sie Joda DateTime benötigen, verwenden Sie einfach den Dropwizard DateTimeParam .

Wenn die obige Liste nicht Ihren Anforderungen entspricht, definieren Sie Ihre eigene, indem Sie AbstractParam erweitern. Analysemethode überschreiben. Wenn Sie die Kontrolle über den Fehlerantworttext benötigen, überschreiben Sie die Fehlermethode.

Ein guter Artikel von Coda Hale dazu ist unter http://codahale.com/what-makes-jersey-interesting-parameter-classes/

import io.dropwizard.jersey.params.AbstractParam;

import java.util.Date;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class DateParam extends AbstractParam<Date> {

    public DateParam(String input) {
        super(input);
    }

    @Override
    protected Date parse(String input) throws Exception {
        return new Date(input);
    }

    @Override
    protected Response error(String input, Exception e) {
        // customize response body if you like here by specifying entity
        return Response.status(Status.BAD_REQUEST).build();
    }
}

Der Datumskonstruktor (String arg) ist veraltet. Ich würde Java 8-Datumsklassen verwenden, wenn Sie Java 8 verwenden. Andernfalls wird eine joda-Datumszeit empfohlen.

Srikanth
quelle
1

Dies ist eigentlich das richtige Verhalten. Jersey wird versuchen, einen Handler für Ihre Eingabe zu finden und aus der bereitgestellten Eingabe ein Objekt zu erstellen. In diesem Fall wird versucht, ein neues Date-Objekt mit dem Wert X zu erstellen, der dem Konstruktor bereitgestellt wird. Da dies ein ungültiges Datum ist, gibt Jersey gemäß Konvention 404 zurück.

Was Sie tun können, ist, das Geburtsdatum neu zu schreiben und als Zeichenfolge zu setzen. Versuchen Sie dann zu analysieren. Wenn Sie nicht das bekommen, was Sie wollen, können Sie jede gewünschte Ausnahme über einen der Ausnahmezuordnungsmechanismen auslösen (es gibt mehrere ).

ACV
quelle
1

Ich stand vor dem gleichen Problem.

Ich wollte alle Fehler an einem zentralen Ort erfassen und transformieren.

Es folgt der Code, wie ich damit umgegangen bin.

Erstellen Sie die folgende Klasse, die implementiert ExceptionMapperund hinzugefügt wird@Provider diese Klasse Anmerkungen versehen. Dadurch werden alle Ausnahmen behandelt.

Überschreiben Sie die toResponseMethode und geben Sie das Antwortobjekt zurück, das mit benutzerdefinierten Daten gefüllt ist.

//ExceptionMapperProvider.java
/**
 * exception thrown by restful endpoints will be caught and transformed here
 * so that client gets a proper error message
 */
@Provider
public class ExceptionMapperProvider implements ExceptionMapper<Throwable> {
    private final ErrorTransformer errorTransformer = new ErrorTransformer();

    public ExceptionMapperProvider() {

    }

    @Override
    public Response toResponse(Throwable throwable) {
        //transforming the error using the custom logic of ErrorTransformer 
        final ServiceError errorResponse = errorTransformer.getErrorResponse(throwable);
        final ResponseBuilder responseBuilder = Response.status(errorResponse.getStatus());

        if (errorResponse.getBody().isPresent()) {
            responseBuilder.type(MediaType.APPLICATION_JSON_TYPE);
            responseBuilder.entity(errorResponse.getBody().get());
        }

        for (Map.Entry<String, String> header : errorResponse.getHeaders().entrySet()) {
            responseBuilder.header(header.getKey(), header.getValue());
        }

        return responseBuilder.build();
    }
}

// ErrorTransformer.java
/**
 * Error transformation logic
 */
public class ErrorTransformer {
    public ServiceError getErrorResponse(Throwable throwable) {
        ServiceError serviceError = new ServiceError();
        //add you logic here
        serviceError.setStatus(getStatus(throwable));
        serviceError.setBody(getBody(throwable));
        serviceError.setHeaders(getHeaders(throwable));

    }
    private String getStatus(Throwable throwable) {
        //your logic
    }
    private Optional<String> getBody(Throwable throwable) {
        //your logic
    }
    private Map<String, String> getHeaders(Throwable throwable) {
        //your logic
    }
}

//ServiceError.java
/**
 * error data holder
 */
public class ServiceError {
    private int status;
    private Map<String, String> headers;
    private Optional<String> body;
    //setters and getters
}
suraj.tripathi
quelle
1

Ansatz 1: Durch Erweitern der WebApplicationException-Klasse

Erstellen Sie eine neue Ausnahme, indem Sie WebApplicationException erweitern

public class RestException extends WebApplicationException {

         private static final long serialVersionUID = 1L;

         public RestException(String message, Status status) {
         super(Response.status(status).entity(message).type(MediaType.TEXT_PLAIN).build());
         }
}

Wirf jetzt bei Bedarf 'RestException'.

public static Employee getEmployee(int id) {

         Employee emp = employees.get(id);

         if (emp == null) {
                 throw new RestException("Employee with id " + id + " not exist", Status.NOT_FOUND);
         }
         return emp;
}

Die vollständige Bewerbung finden Sie unter diesem Link .

Ansatz 2: Implementieren Sie ExceptionMapper

Der folgende Mapper behandelt die Ausnahme vom Typ 'DataNotFoundException'.

@Provider
public class DataNotFoundExceptionMapper implements
        ExceptionMapper<DataNotFoundException> {

    @Override
    public Response toResponse(DataNotFoundException ex) {
        ErrorMessage model = new ErrorMessage(ex.getErrorCode(),
                ex.getMessage());
        return Response.status(Status.NOT_FOUND).entity(model).build();
    }

}

Die vollständige Bewerbung finden Sie unter diesem Link .

Hari Krishna
quelle
0

Nur als Erweiterung zu @Steven Lavine Antwort für den Fall, dass Sie das Browser-Anmeldefenster öffnen möchten. Es fiel mir schwer, die Antwort ( MDN-HTTP-Authentifizierung) ordnungsgemäß zurückzugeben ) vom Filter falls der Benutzer noch nicht authentifiziert war

Dies hat mir geholfen, die Antwort zu erstellen, um die Browser-Anmeldung zu erzwingen. Beachten Sie die zusätzliche Änderung der Header. Dadurch wird der Statuscode auf 401 gesetzt und der Header festgelegt, der den Browser veranlasst, das Dialogfeld Benutzername / Passwort zu öffnen.

// The extended Exception class
public class NotLoggedInException extends WebApplicationException {
  public NotLoggedInException(String message) {
    super(Response.status(Response.Status.UNAUTHORIZED)
      .entity(message)
      .type(MediaType.TEXT_PLAIN)
      .header("WWW-Authenticate", "Basic realm=SecuredApp").build()); 
  }
}

// Usage in the Filter
if(headers.get("Authorization") == null) { throw new NotLoggedInException("Not logged in"); }
Omnibyte
quelle