JAX-RS / Jersey comment personnaliser la gestion des erreurs?

216

J'apprends JAX-RS (alias JSR-311) en utilisant Jersey. J'ai réussi à créer une ressource racine et je joue avec les paramètres:

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

Cela fonctionne très bien et gère n'importe quel format dans les paramètres régionaux actuels qui est compris par le constructeur Date (chaîne) (comme AAAA / mm / jj et mm / jj / AAAA). Mais si je fournis une valeur non valide ou non comprise, j'obtiens une réponse 404.

Par exemple:

GET /hello?name=Mark&birthDate=X

404 Not Found

Comment puis-je personnaliser ce comportement? Peut-être un code de réponse différent (probablement "400 Bad Request")? Qu'en est-il de l'enregistrement d'une erreur? Peut-être ajouter une description du problème ("format de date incorrect") dans un en-tête personnalisé pour faciliter le dépannage? Ou renvoyer une réponse d'erreur complète avec des détails, ainsi qu'un code d'état 5xx?

Mark Renouf
la source

Réponses:

271

Il existe plusieurs approches pour personnaliser le comportement de gestion des erreurs avec JAX-RS. Voici trois des moyens les plus simples.

La première approche consiste à créer une classe Exception qui étend WebApplicationException.

Exemple:

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

Et pour lancer cette nouvelle exception, il vous suffit de:

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

Notez que vous n'avez pas besoin de déclarer l'exception dans une clause throws car WebApplicationException est une exception d'exécution. Cela retournera une réponse 401 au client.

La deuxième approche, plus simple, consiste à simplement construire une instance de WebApplicationException directement dans votre code. Cette approche fonctionne tant que vous n'avez pas à implémenter vos propres exceptions d'application.

Exemple:

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

Ce code renvoie également un 401 au client.

Bien sûr, ce n'est qu'un exemple simple. Vous pouvez rendre l'exception beaucoup plus complexe si nécessaire, et vous pouvez générer le code de réponse http dont vous avez besoin.

Une autre approche consiste à encapsuler une exception existante, peut-être une ObjectNotFoundExceptionavec une petite classe wrapper qui implémente l' ExceptionMapperinterface annotée avec une @Providerannotation. Cela indique au runtime JAX-RS que si l'exception encapsulée est déclenchée, retourne le code de réponse défini dans le ExceptionMapper.

Steven Levine
la source
3
Dans votre exemple, l'appel à super () doit être légèrement différent: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); Merci pour la perspicacité cependant.
Jon Onstott
65
Dans le scénario mentionné dans la question, vous n'aurez pas la possibilité de lever une exception, car Jersey lèvera une exception car il ne pourra pas créer d'instance d'objet Date à partir de la valeur d'entrée. Existe-t-il un moyen d'intercepter l'exception de Jersey? Il existe cependant une interface ExceptionMapper qui intercepte également les exceptions levées par la méthode (get dans ce cas).
Rejeev Divakaran
7
Comment éviter que l'exception n'apparaisse dans vos journaux de serveur si le 404 est un cas valide et non une erreur (c'est-à-dire chaque fois que vous recherchez une ressource, juste pour voir si elle existe déjà, avec votre approche une trace de pile apparaît sur le serveur journaux).
Guido
3
Il convient de mentionner que Jersey 2.x définit des exceptions pour certains des codes d'erreur HTTP les plus courants. Ainsi, au lieu de définir vos propres sous-classes de WebApplication, vous pouvez utiliser celles intégrées comme BadRequestException et NotAuthorizedException. Regardez les sous-classes de javax.ws.rs.ClientErrorException par exemple. Notez également que vous pouvez fournir une chaîne de détails aux constructeurs. Par exemple: lever une nouvelle BadRequestException ("La date de début doit précéder la date de fin");
Bampfer
1
vous avez oublié de mentionner une autre approche: la mise en œuvre de l' ExceptionMapperinterface (qui est une meilleure approche que l'extension). Voir plus ici 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();
}
}

Créez la classe ci-dessus. Cela gérera 404 (NotFoundException) et ici, dans la méthode toResponse, vous pouvez donner votre réponse personnalisée. De même, il y a ParamException etc. que vous devez mapper pour fournir des réponses personnalisées.

Arnav
la source
Vous pouvez également utiliser les outils ExceptionMapper <Exception> pour les exceptions génériques
Saurabh
1
Cela gérerait également les WebApplicationExceptions levées par le client JAX-RS, cachant l'origine de l'erreur. Mieux vaut avoir une exception personnalisée (non dérivée de WebApplicationException) ou lancer WebApplications avec une réponse complète. Les WebApplicationExceptions levées par le client JAX-RS doivent être traitées directement lors de l'appel, sinon la réponse d'un autre service est transmise en tant que réponse de votre service bien qu'il s'agisse d'une erreur de serveur interne non gérée.
Markus Kull
38

Jersey lève une exception com.sun.jersey.api.ParamException lorsqu'il ne parvient pas à démêler les paramètres, une solution consiste donc à créer un ExceptionMapper qui gère ces types d'exceptions:

@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
la source
où dois-je créer ce mappeur spécifiquement pour Jersey pour l'enregistrer?
Patricio
1
Tout ce que vous avez à faire est d'ajouter l'annotation @Provider, voir ici pour plus de détails: stackoverflow.com/questions/15185299/…
Jan Kronquist
27

Vous pouvez également écrire une classe réutilisable pour les variables annotées 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());
  }
}

puis utilisez-le comme ceci:

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

Bien que la gestion des erreurs soit triviale dans ce cas (lancement d'une réponse 400), l'utilisation de cette classe vous permet de factoriser la gestion des paramètres en général, ce qui peut inclure la journalisation, etc.

Charlie Brooking
la source
J'essaie d'ajouter un gestionnaire de paramètres de requête personnalisé dans Jersey (migration depuis CXF), cela ressemble remarquablement à ce que je fais, mais je ne sais pas comment installer / créer un nouveau fournisseur. Votre classe ci-dessus ne me montre pas cela. J'utilise des objets JodaTime DateTime pour QueryParam et je n'ai pas de fournisseur pour les décoder. Est-ce aussi simple que de le sous-classer, de lui donner un constructeur String et de le gérer?
Christian Bongiorno
1
Créez simplement une classe comme DateParamcelle ci-dessus qui encapsule un org.joda.time.DateTimeau lieu de java.util.Calendar. Vous l'utilisez avec @QueryParamplutôt que DateTimelui-même.
Charlie Brooking
1
Si vous utilisez Joda DateTime, le maillot est livré avec DateTimeParam pour que vous puissiez l'utiliser directement. Pas besoin d'écrire le vôtre. Voir github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Srikanth
Je vais ajouter ceci parce que c'est super utile, mais seulement si vous utilisez Jackson avec Jersey. Jackson 2.x a un JodaModulequi peut être enregistré avec la ObjectMapper registerModulesméthode. Il peut gérer toutes les conversions de type joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev
11

Une solution évidente: prenez une chaîne, convertissez-vous en date vous-même. De cette façon, vous pouvez définir le format souhaité, intercepter les exceptions et relancer ou personnaliser l'erreur envoyée. Pour l'analyse, SimpleDateFormat devrait fonctionner correctement.

Je suis sûr qu'il existe également des moyens de connecter des gestionnaires pour les types de données, mais peut-être que peu de code simple est tout ce dont vous avez besoin dans ce cas.

StaxMan
la source
7

Moi aussi j'aime StaxMan implémenterait probablement ce QueryParam en tant que chaîne, puis gérerais la conversion, en la reconduisant si nécessaire.

Si le comportement spécifique aux paramètres régionaux est le comportement souhaité et attendu, vous utiliseriez ce qui suit pour renvoyer l'erreur 400 BAD REQUEST:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Voir JavaDoc pour javax.ws.rs.core.Response.Status pour plus d'options.

dshaw
la source
4

La documentation de @QueryParam indique

"Le type T du paramètre, du champ ou de la propriété annoté doit:

1) Être un type primitif
2) Avoir un constructeur qui accepte un seul argument String
3) Avoir une méthode statique nommée valueOf ou fromString qui accepte un seul argument String (voir, par exemple, Integer.valueOf (String))
4) Avoir un implémentation enregistrée de javax.ws.rs.ext.ParamConverterProvider JAX-RS extension SPI qui renvoie une instance javax.ws.rs.ext.ParamConverter capable d'une conversion "de chaîne" pour le type.
5) Soyez List, Set ou SortedSet, où T satisfait 2, 3 ou 4 ci-dessus. La collection résultante est en lecture seule. "

Si vous souhaitez contrôler la réponse envoyée à l'utilisateur lorsque le paramètre de requête sous forme de chaîne ne peut pas être converti en votre type T, vous pouvez lever WebApplicationException. Dropwizard est livré avec les classes * Param suivantes que vous pouvez utiliser selon vos besoins.

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

Si vous avez besoin de Joda DateTime, utilisez simplement Dropwizard DateTimeParam .

Si la liste ci-dessus ne correspond pas à vos besoins, définissez les vôtres en étendant AbstractParam. Remplacer la méthode d'analyse. Si vous avez besoin de contrôler le corps de réponse d'erreur, remplacez la méthode d'erreur.

Un bon article de Coda Hale à ce sujet est à 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();
    }
}

Le constructeur Date (String arg) est obsolète. J'utiliserais des classes de date Java 8 si vous êtes sur Java 8. Sinon, l'heure de date Joda est recommandée.

Srikanth
la source
1

C'est le comportement correct en fait. Jersey essaiera de trouver un gestionnaire pour votre entrée et essaiera de construire un objet à partir de l'entrée fournie. Dans ce cas, il essaiera de créer un nouvel objet Date avec la valeur X fournie au constructeur. Puisqu'il s'agit d'une date invalide, par convention, Jersey renverra 404.

Ce que vous pouvez faire est de réécrire et de mettre la date de naissance sous forme de chaîne, puis essayez d'analyser et si vous n'obtenez pas ce que vous voulez, vous êtes libre de lever toute exception que vous voulez par l'un des mécanismes de mappage d'exception (il existe plusieurs ).

ACV
la source
1

J'étais confronté au même problème.

Je voulais attraper toutes les erreurs à un endroit central et les transformer.

Voici le code de la façon dont je l'ai géré.

Créez la classe suivante qui implémente ExceptionMapperet ajoutez des @Providerannotations sur cette classe. Cela gérera toutes les exceptions.

Remplacez la toResponseméthode et renvoyez l'objet Response rempli avec des données personnalisées.

//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
la source
1

Approche 1: en étendant la classe WebApplicationException

Créer une nouvelle exception en étendant 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());
         }
}

Lancez maintenant «RestException» chaque fois que nécessaire.

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

Vous pouvez voir la demande complète sur ce lien .

Approche 2: implémenter ExceptionMapper

Le mappeur suivant gère l'exception de type '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();
    }

}

Vous pouvez voir la demande complète sur ce lien .

Hari Krishna
la source
0

Tout comme une extension de @Steven Lavine, répondez au cas où vous voudriez ouvrir la fenêtre de connexion du navigateur. J'ai trouvé difficile de renvoyer correctement la réponse ( authentification HTTP MDN ) du filtre au cas où l'utilisateur ne serait pas encore authentifié

Cela m'a aidé à construire la réponse pour forcer la connexion au navigateur, notez la modification supplémentaire des en-têtes. Cela définira le code d'état sur 401 et définira l'en-tête qui amènera le navigateur à ouvrir la boîte de dialogue nom d'utilisateur / mot de passe.

// 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
la source