web-dev-qa-db-fra.com

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

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 tous les formats des paramètres régionaux en vigueur, compris par le constructeur Date (String) (tels que YYYY/mm/jj et mm/jj/AAAA). Mais si je fournis une valeur invalide ou non comprise, je reçois 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 la journalisation 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?

209
Mark Renouf

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

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 exception nouvellement créée, vous devez simplement:

@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 qu'il n'est pas nécessaire 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 à créer une instance de WebApplicationException directement dans votre code. Cette approche fonctionne tant que vous n'avez pas à implémenter vos propres applications Exceptions.

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 exception ObjectNotFoundException, avec une petite classe wrapper qui implémente l'interface ExceptionMapper annotée avec une annotation @Provider. Cela indique au moteur d'exécution JAX-RS que si l'exception encapsulée est déclenchée, renvoyer le code de réponse défini dans ExceptionMapper.

267
Steven Levine
@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éer au-dessus de la classe. Cela gérera 404 (NotFoundException) et ici, dans la méthode toResponse, vous pouvez donner votre réponse personnalisée. De même, il existe des paramètres ParamException, etc. que vous devez mapper pour fournir des réponses personnalisées.

67
Arnav

Jersey lève une exception com.Sun.jersey.api.ParamException lorsqu'il ne parvient pas à annuler les paramètres, ainsi une solution consiste à créer un ExceptionMapper qui gère les types d'exceptions suivants:

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

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 (envoi 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.

26
Charlie Brooking

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

Je suis sûr qu’il existe des moyens d’accrocher les gestionnaires pour les types de données, mais un peu de code simple suffit peut-être dans ce cas.

11
StaxMan

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

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

throw new WebApplicationException(Response.Status.BAD_REQUEST);

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

7
dshaw

La documentation de @QueryParam indique

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

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 une implémentation enregistrée de l'extension JAX-RS javax.ws.rs.ext.ParamConverterProvider SPI renvoyant une instance javax.ws.rs.exts.ParamConverter capable d'un "de string "conversion 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 à 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 lancer WebApplicationException. Dropwizard est fourni avec les classes * Param suivantes, que vous pouvez utiliser pour répondre à 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 le vôtre en développant AbstractParam. Ignorer la méthode d'analyse. Si vous avez besoin de contrôler le corps de la réponse d'erreur, remplacez la méthode d'erreur.

Le bon article de Coda Hale sur ceci 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. Je voudrais utiliser Java 8 classes de date si vous êtes sur Java 8. Sinon, joda date time est recommandé.

4
Srikanth

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. Comme il s’agit d’une date invalide, Jersey renverra 404 par convention.

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

1
ACV

Juste comme une extension de @Steven Lavine répondre au cas où vous voudriez ouvrir la fenêtre de connexion du navigateur. J'ai eu du mal à renvoyer correctement la réponse ( authentification MDN HTTP ) à partir du filtre au cas où l'utilisateur ne serait pas encore authentifié

Cela m'a aidé à construire la réponse pour forcer la connexion du 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ène 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"); }
0
Omnibyte