JAX-RS / Jersey come personalizzare la gestione degli errori?


216

Sto imparando JAX-RS (aka, JSR-311) usando Jersey. Ho creato con successo una risorsa radice e sto giocando con i parametri:

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

Funziona benissimo e gestisce qualsiasi formato nella locale corrente che è compreso dal costruttore Date (String) (come AAAA / mm / gg e mm / gg / AAAA). Ma se fornisco un valore non valido o non compreso, ottengo una risposta 404.

Per esempio:

GET /hello?name=Mark&birthDate=X

404 Not Found

Come posso personalizzare questo comportamento? Forse un codice di risposta diverso (probabilmente "400 Bad Request")? Che dire della registrazione di un errore? Forse aggiungere una descrizione del problema ("formato data errato") in un'intestazione personalizzata per facilitare la risoluzione dei problemi? O restituire un'intera risposta di errore con i dettagli, insieme a un codice di stato 5xx?

Risposte:


271

Esistono diversi approcci per personalizzare il comportamento di gestione degli errori con JAX-RS. Ecco tre dei modi più semplici.

Il primo approccio consiste nel creare una classe Exception che estende WebApplicationException.

Esempio:

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

E per lanciare questa eccezione appena creata, devi semplicemente:

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

Si noti che non è necessario dichiarare l'eccezione in una clausola di proiezione perché WebApplicationException è un'eccezione di runtime. Ciò restituirà una risposta 401 al client.

Il secondo e più semplice approccio è semplicemente costruire un'istanza del WebApplicationExceptiondirettamente nel tuo codice. Questo approccio funziona fino a quando non è necessario implementare le eccezioni dell'applicazione.

Esempio:

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

Anche questo codice restituisce un 401 al client.

Certo, questo è solo un semplice esempio. Se necessario, puoi rendere l'eccezione molto più complessa e generare qualsiasi codice di risposta http di cui hai bisogno.

Un altro approccio è quello di racchiudere un'eccezione esistente, magari una ObjectNotFoundExceptioncon una piccola classe wrapper che implementa l' ExceptionMapperinterfaccia annotata con @Providerun'annotazione. Questo dice al runtime JAX-RS che se l'eccezione racchiusa viene sollevata, restituisce il codice di risposta definito in ExceptionMapper.


3
Nel tuo esempio, la chiamata a super () dovrebbe essere leggermente diversa: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); Grazie per l'intuizione però.
Jon Onstott,

65
Nello scenario menzionato nella domanda, non avrai la possibilità di lanciare un'eccezione, poiché Jersey genererà un'eccezione in quanto non sarà in grado di creare un'istanza dell'oggetto Date dal valore di input. C'è un modo per intercettare l'eccezione Jersey? Esiste un'interfaccia ExceptionMapper, tuttavia che intercetta anche le eccezioni generate dal metodo (in questo caso).
Rejeev Divakaran,

7
Come evitare che l'eccezione compaia nei registri del server se il 404 è un caso valido e non un errore (ad esempio ogni volta che si esegue una query per una risorsa, solo per vedere se esiste già, con il proprio approccio appare una traccia stack nel server log).
Guido,

3
Vale la pena ricordare che Jersey 2.x definisce le eccezioni per alcuni dei codici di errore HTTP più comuni. Quindi, invece di definire le proprie sottoclassi di WebApplication, è possibile utilizzare quelle integrate come BadRequestException e NotAuthorizedException. Guarda le sottoclassi di javax.ws.rs.ClientErrorException per esempio. Si noti inoltre che è possibile fornire una stringa di dettagli ai costruttori. Ad esempio: lanciare la nuova BadRequestException ("La data di inizio deve precedere la data di fine");
Bampfer,

1
hai dimenticato di menzionare ancora un altro approccio: implementare l' ExceptionMapperinterfaccia (che è un approccio migliore che si estende). Vedi di più qui vvirlan.wordpress.com/2015/10/19/…
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();
}
}

Crea sopra la classe. Questo gestirà 404 (NotFoundException) e qui nel metodo toResponse puoi dare la tua risposta personalizzata. Allo stesso modo ci sono ParamException ecc. Che dovresti mappare per fornire risposte personalizzate.


Puoi usare gli attrezzi ExceptionMapper <Exception> anche per le eccezioni generiche
Saurabh,

1
Ciò gestirà anche WebApplicationExceptions generata dal client JAX-RS, nascondendo l'origine dell'errore. Meglio avere un'eccezione personalizzata (non derivata da WebApplicationException) o generare WebApplications con risposta completa. Le eccezioni WebApplication lanciate dal client JAX-RS devono essere gestite direttamente alla chiamata, altrimenti la risposta di un altro servizio viene trasmessa come risposta del servizio, sebbene si tratti di un errore interno del server non gestito.
Markus Kull,

38

Jersey genera un com.sun.jersey.api.ParamException quando non riesce a smascherare i parametri, quindi una soluzione è quella di creare un ExceptionMapper che gestisca questi tipi di eccezioni:

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

dove devo creare questo mapper appositamente per Jersey per registrarlo?
Patricio,

1
Tutto quello che dovete fare è aggiungere l'annotazione @Provider, vedi qui per maggiori dettagli: stackoverflow.com/questions/15185299/...
Jan Kronquist

27

È inoltre possibile scrivere una classe riutilizzabile per le variabili annotate da QueryParam

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

quindi usalo in questo modo:

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

Sebbene in questo caso la gestione degli errori sia banale (generando una risposta 400), l'utilizzo di questa classe consente di escludere la gestione dei parametri in generale, che potrebbe includere la registrazione, ecc.


Sto provando ad aggiungere un gestore di parametri di query personalizzato in Jersey (migrazione da CXF) che sembra notevolmente simile a quello che sto facendo, ma non so come installare / creare un nuovo provider. La tua lezione di cui sopra non mi mostra questo. Sto usando gli oggetti JodaTime DateTime per QueryParam e non ho un provider per decodificarli. È facile come sottoclassarlo, dandogli un costruttore di stringhe e gestendolo?
Christian Bongiorno,

1
Basta creare una classe come DateParamquella sopra che avvolge un org.joda.time.DateTimeinvece di java.util.Calendar. Lo usi con @QueryParampiuttosto che con se DateTimestesso.
Charlie Brooking,

1
Se stai usando Joda DateTime, la maglia viene fornita con DateTimeParam da usare direttamente. Non c'è bisogno di scriverne uno tuo. Vedi github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Srikanth

Ho intenzione di aggiungere questo perché è super utile, ma solo se si utilizza Jackson con Jersey. Jackson 2.x ha un JodaModuleche può essere registrato con il ObjectMapper registerModulesmetodo. Può gestire tutte le conversioni di tipo joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev il

11

Una soluzione ovvia: prendi una stringa, converti da solo. In questo modo è possibile definire il formato desiderato, rilevare le eccezioni e ripetere o personalizzare l'errore inviato. Per l'analisi, SimpleDateFormat dovrebbe funzionare correttamente.

Sono sicuro che ci sono modi per agganciare i gestori anche ai tipi di dati, ma forse in questo caso è sufficiente un po 'di codice semplice.


7

Anche a me piace StaxMan probabilmente implementerei QueryParam come una stringa, quindi gestirò la conversione, ricodificandola se necessario.

Se il comportamento specifico della locale è il comportamento desiderato e previsto, utilizzare il seguente per restituire l'errore 400 BAD REQUEST:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Vedere JavaDoc per javax.ws.rs.core.Response.Status per ulteriori opzioni.


4

La documentazione di @QueryParam dice

"Il tipo T del parametro, campo o proprietà annotato deve:

1) Essere un tipo primitivo
2) Avere un costruttore che accetta un singolo argomento String
3) Avere un metodo statico denominato valueOf o fromString che accetta un singolo argomento String (vedere, ad esempio, Integer.valueOf (String))
4) Avere un implementazione registrata di javax.ws.rs.ext.ParamConverterProvider SPI dell'estensione JAX-RS che restituisce un'istanza javax.ws.rs.ext.ParamConverter in grado di eseguire una conversione "da stringa" per il tipo.
5) Be List, Set o SortedSet, dove T soddisfa 2, 3 o 4 sopra. La raccolta risultante è di sola lettura. "

Se vuoi controllare quale risposta viene inviata all'utente quando il parametro di query in formato String non può essere convertito nel tuo tipo T, puoi lanciare WebApplicationException. Dropwizard viene fornito con le seguenti classi di parametri * che è possibile utilizzare per le proprie esigenze.

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

Se hai bisogno di Joda DateTime, usa Dropwizard DateTimeParam .

Se l'elenco sopra non soddisfa le tue esigenze, definisci il tuo estendendo AbstractParam. Sostituisci metodo di analisi. Se è necessario il controllo sul corpo della risposta dell'errore, sostituire il metodo dell'errore.

Un buon articolo di Coda Hale su questo è su 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();
    }
}

Il costruttore Date (String arg) è obsoleto. Vorrei utilizzare le classi di date Java 8 se si è su Java 8. Altrimenti si consiglia l'ora della data joda.


1

Questo è il comportamento corretto in realtà. Jersey proverà a trovare un gestore per l'input e proverà a costruire un oggetto dall'input fornito. In questo caso proverà a creare un nuovo oggetto Date con il valore X fornito al costruttore. Poiché questa è una data non valida, per convenzione Jersey restituirà 404.

Quello che puoi fare è riscrivere e mettere la data di nascita come una stringa, quindi provare ad analizzare e se non ottieni quello che vuoi, sei libero di lanciare qualsiasi eccezione tu voglia da uno qualsiasi dei meccanismi di mappatura delle eccezioni (ci sono diversi ).


1

Stavo affrontando lo stesso problema.

Volevo catturare tutti gli errori in un posto centrale e trasformarli.

Di seguito è riportato il codice per come l'ho gestito.

Creare la seguente classe che implementa ExceptionMappere aggiungere @Providerannotazioni su questa classe. Questo gestirà tutte le eccezioni.

Sostituisci toResponsemetodo e restituisce l'oggetto Response popolato con dati personalizzati.

//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
}

1

Approccio 1: estendendo la classe WebApplicationException

Crea una nuova eccezione estendendo WebApplicationException

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

Ora lancia 'RestException' quando richiesto.

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

Puoi vedere l'applicazione completa a questo link .

Approccio 2: implementare ExceptionMapper

Il seguente mapper gestisce l'eccezione del tipo "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();
    }

}

Puoi vedere l'applicazione completa a questo link .


0

Proprio come un'estensione alla risposta di @Steven Lavine nel caso in cui si desidera aprire la finestra di accesso del browser. Ho trovato difficile restituire correttamente la risposta ( autenticazione HTTP MDN ) dal filtro nel caso in cui l'utente non fosse ancora autenticato

Questo mi ha aiutato a creare la risposta per forzare l'accesso al browser, notare la modifica aggiuntiva delle intestazioni. Ciò imposterà il codice di stato su 401 e imposterà l'intestazione che induce il browser ad aprire la finestra di dialogo nome utente / password.

// 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"); }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.