279 votes

JAX-RS - Comment retourner JSON et le code d'état HTTP ensemble ?

J'écris une application web REST (NetBeans 6.9, JAX-RS, TopLink Essentials) et j'essaie de renvoyer du JSON. y Code d'état HTTP. J'ai un code prêt et fonctionnel qui renvoie JSON lorsque la méthode HTTP GET est appelée par le client. Essentiellement :

@Path("get/id")
@GET
@Produces("application/json")
public M_ getMachineToUpdate(@PathParam("id") String id) {

    // some code to return JSON ...

    return myJson;
}

Mais je également vous souhaitez renvoyer un code d'état HTTP (500, 200, 204, etc.) avec les données JSON.

J'ai essayé d'utiliser HttpServletResponse :

response.sendError("error message", 500);

Mais cela a fait croire au navigateur qu'il s'agissait d'une "vraie" 500 et la page Web de sortie était une page d'erreur HTTP 500 normale.

Je veux renvoyer un code d'état HTTP pour que mon JavaScript côté client puisse gérer une logique qui en dépend (par exemple, afficher le code et le message d'erreur sur une page HTML). Est-ce possible ou les codes d'état HTTP ne doivent-ils pas être utilisés pour ce genre de choses ?

2 votes

Quelle est la différence entre les 500 que vous voulez (irréels ? :) ) et les 500 réels ?

0 votes

@razor Ici, le vrai 500 signifie une page d'erreur HTML au lieu d'une réponse JSON.

0 votes

Le navigateur web n'a pas été conçu pour travailler avec JSON, mais avec des pages HTML, donc si vous répondez avec 500 (et même un corps de message), le navigateur peut vous montrer juste un message d'erreur (cela dépend vraiment de l'implémentation du navigateur), juste parce que c'est utile pour un utilisateur normal.

389voto

hisdrewness Points 2643

Voici un exemple :

@GET
@Path("retrieve/{uuid}")
public Response retrieveSomething(@PathParam("uuid") String uuid) {
    if(uuid == null || uuid.trim().length() == 0) {
        return Response.serverError().entity("UUID cannot be blank").build();
    }
    Entity entity = service.getById(uuid);
    if(entity == null) {
        return Response.status(Response.Status.NOT_FOUND).entity("Entity not found for UUID: " + uuid).build();
    }
    String json = //convert entity to json
    return Response.ok(json, MediaType.APPLICATION_JSON).build();
}

Jetez un coup d'œil à la Réponse classe.

Notez que vous devez toujours spécifier un type de contenu, surtout si vous transmettez plusieurs types de contenu, mais si chaque message est représenté en JSON, vous pouvez simplement annoter la méthode avec @Produces("application/json")

13 votes

Cela fonctionne, mais ce que je n'aime pas dans la valeur de retour de la réponse, c'est qu'à mon avis, elle pollue votre code, surtout en ce qui concerne les clients qui essaient de l'utiliser. Si vous fournissez une interface retournant une Response à un tiers, il ne sait pas quel type vous retournez réellement. Spring rend les choses plus claires avec une annotation, très utile si vous retournez toujours un code de statut (par exemple HTTP 204).

20 votes

Rendre cette classe générique (Response<T>) serait une amélioration intéressante de jax-rs, pour avoir les avantages des deux alternatives.

50 votes

Il n'est pas nécessaire de convertir l'entité en json d'une manière ou d'une autre. Vous pouvez faire return Response.status(Response.Status.Forbidden).entity(myPOJO).bu‌​ild(); Fonctionne comme si vous aviez return myPOJO; mais avec un paramétrage supplémentaire du code HTTP-Status.

211voto

Pierre Henry Points 1940

Il existe plusieurs cas d'utilisation pour définir les codes d'état HTTP dans un service web REST, et au moins un n'était pas suffisamment documenté dans les réponses existantes (c'est-à-dire lorsque vous utilisez la sérialisation JSON/XML auto-magique à l'aide de JAXB, et que vous voulez renvoyer un objet à sérialiser, mais aussi un code d'état différent du 200 par défaut).

Je vais donc essayer d'énumérer les différents cas d'utilisation et les solutions pour chacun d'entre eux :

1. Code d'erreur (500, 404,...)

Le cas d'utilisation le plus courant lorsque vous souhaitez renvoyer un code d'état différent de celui de l'utilisateur. 200 OK c'est lorsqu'une erreur se produit.

Par exemple :

  • une entité est demandée mais elle n'existe pas (404)
  • la demande est sémantiquement incorrecte (400)
  • l'utilisateur n'est pas autorisé (401)
  • il y a un problème avec la connexion à la base de données (500)
  • etc.

a) Lancer une exception

Dans ce cas, je pense que la façon la plus propre de traiter le problème est de lancer une exception. Cette exception sera gérée par un ExceptionMapper qui traduira l'exception en une réponse avec le code d'erreur approprié.

Vous pouvez utiliser l'option par défaut ExceptionMapper qui est livré préconfiguré avec Jersey (et je suppose que c'est la même chose avec d'autres implémentations) et lancer n'importe quelle sous-classe existante de javax.ws.rs.WebApplicationException . Il s'agit de types d'exception prédéfinis qui sont mappés à différents codes d'erreur, par exemple :

  • BadRequestException (400)
  • InternalServerErrorException (500)
  • NotFoundException (404)

Etc. Vous pouvez trouver la liste ici : API

Vous pouvez aussi définir vos propres exceptions et ExceptionMapper et d'ajouter ces mappeurs à Jersey par le biais des @Provider annotation ( source de cet exemple ):

public class MyApplicationException extends Exception implements Serializable
{
    private static final long serialVersionUID = 1L;
    public MyApplicationException() {
        super();
    }
    public MyApplicationException(String msg)   {
        super(msg);
    }
    public MyApplicationException(String msg, Exception e)  {
        super(msg, e);
    }
}

Prestataire :

    @Provider
    public class MyApplicationExceptionHandler implements ExceptionMapper<MyApplicationException> 
    {
        @Override
        public Response toResponse(MyApplicationException exception) 
        {
            return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();  
        }
    }

Remarque : vous pouvez également écrire des ExceptionMappers pour les types d'exception existants que vous utilisez.

b) Utiliser le constructeur de réponses

Une autre façon de définir un code d'état est d'utiliser une fonction Response pour construire une réponse avec le code prévu.

Dans ce cas, le type de retour de votre méthode doit être javax.ws.rs.core.Response . Ce processus est décrit dans diverses autres réponses, comme la réponse acceptée par hisdrewness, et ressemble à ceci :

@GET
@Path("myresource({id}")
public Response retrieveSomething(@PathParam("id") String id) {
    ...
    Entity entity = service.getById(uuid);
    if(entity == null) {
        return Response.status(Response.Status.NOT_FOUND).entity("Resource not found for ID: " + uuid).build();
    }
    ...
}

2. Un succès, mais pas 200

Un autre cas où vous voulez définir l'état de retour est celui où l'opération a réussi, mais où vous voulez renvoyer un code de réussite différent de 200, ainsi que le contenu que vous renvoyez dans le corps.

Un cas d'utilisation fréquent est celui de la création d'une nouvelle entité ( POST ) et veulent renvoyer des informations sur cette nouvelle entité ou peut-être l'entité elle-même, ainsi qu'une demande d'accès à l'information. 201 Created code de statut.

Une approche consiste à utiliser l'objet réponse comme décrit ci-dessus et à définir vous-même le corps de la requête. Cependant, en procédant ainsi, vous perdez la possibilité d'utiliser la sérialisation automatique en XML ou JSON fournie par JAXB.

C'est la méthode originale qui renvoie un objet entité qui sera sérialisé en JSON par JAXB :

@Path("/")
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public User addUser(User user){
    User newuser = ... do something like DB insert ...
    return newuser;
}

Cela renverra une représentation JSON de l'utilisateur nouvellement créé, mais l'état de retour sera 200, et non 201.

Maintenant, le problème est que si je veux utiliser l'option Response pour définir le code de retour, je dois renvoyer un fichier Response dans ma méthode. Comment puis-je toujours retourner l'objet User à sérialiser ?

a) Définir le code sur la réponse de la servlet

Une approche pour résoudre ce problème consiste à obtenir un objet de requête de servlet et à définir manuellement le code de réponse, comme le montre la réponse de Garett Wilson :

@Path("/")
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public User addUser(User user, @Context final HttpServletResponse response){

    User newUser = ...

    //set HTTP code to "201 Created"
    response.setStatus(HttpServletResponse.SC_CREATED);
    try {
        response.flushBuffer();
    }catch(Exception e){}

    return newUser;
}

La méthode renvoie toujours un objet entité et le code d'état sera 201.

Notez que pour que cela fonctionne, j'ai dû vider la réponse. Il s'agit d'une désagréable résurgence de code de bas niveau de l'API Servlet dans notre belle ressource JAX_RS, et pire encore, cela rend les en-têtes non modifiables après cela, car ils ont déjà été envoyés sur le fil.

b) Utiliser l'objet réponse avec l'entité

La meilleure solution, dans ce cas, est d'utiliser l'objet Réponse et de définir l'entité à sérialiser sur cet objet Réponse. Il serait bien de rendre l'objet Response générique pour indiquer le type de l'entité payload dans ce cas, mais ce n'est pas le cas actuellement.

@Path("/")
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public Response addUser(User user){

    User newUser = ...

    return Response.created(hateoas.buildLinkUri(newUser, "entity")).entity(restResponse).build();
}

Dans ce cas, nous utilisons la méthode created de la classe Response builder afin de définir le code d'état 201. Nous passons l'objet entité (utilisateur) à la réponse via la méthode entity().

Le résultat est que le code HTTP est 401 comme nous le voulions, et le corps de la réponse est exactement le même JSON que celui que nous avions auparavant lorsque nous avons simplement renvoyé l'objet User. Il ajoute également un en-tête d'emplacement.

La classe Response possède un certain nombre de méthodes de construction pour différents statuts (stati ?) tels que :

Réponse.acceptée() Response.ok() Réponse.noContent() Réponse.notAcceptable()

NB : l'objet hateoas est une classe d'aide que j'ai développée pour aider à générer les URI des ressources. Vous devrez trouver votre propre mécanisme ici ;)

C'est à peu près tout.

J'espère que cette longue réponse aidera quelqu'un :)

0 votes

Je me demande s'il existe un moyen propre de renvoyer l'objet de données lui-même au lieu de la réponse. Le site flush est en effet sale.

2 votes

C'est juste une de mes bêtes noires : 401 ne veut pas dire l'utilisateur n'est pas autorisé. Cela signifie le client n'est pas autorisé, car le serveur ne sait pas qui vous êtes. Si un utilisateur connecté/autrement reconnu n'est pas autorisé à effectuer une certaine action, le code de réponse correct est 403 Forbidden.

72voto

Garret Wilson Points 2583

La réponse de hisdrewness fonctionnera, mais elle modifie l'approche globale en laissant un fournisseur tel que Jackson+JAXB convertir automatiquement votre objet retourné dans un format de sortie tel que JSON. Inspiré par un projet Apache CXF poste (qui utilise une classe spécifique à CXF), j'ai trouvé une façon de définir le code de réponse qui devrait fonctionner dans n'importe quelle implémentation JAX-RS : injecter un contexte HttpServletResponse et définir manuellement le code de réponse. Par exemple, voici comment définir le code de réponse à CREATED le cas échéant.

@Path("/foos/{fooId}")
@PUT
@Consumes("application/json")
@Produces("application/json")
public Foo setFoo(@PathParam("fooID") final String fooID, final Foo foo, @Context final HttpServletResponse response)
{
  //TODO store foo in persistent storage
  if(itemDidNotExistBefore) //return 201 only if new object; TODO app-specific logic
  {
    response.setStatus(Response.Status.CREATED.getStatusCode());
  }
  return foo;  //TODO get latest foo from storage if needed
}

Amélioration : Après avoir trouvé une autre réponse j'ai appris que l'on peut injecter le HttpServletResponse comme une variable membre, même pour une classe de service singleton (au moins dans RESTEasy) ! C'est une bien meilleure approche que de polluer l'API avec des détails d'implémentation. Cela ressemblerait à ceci :

@Context  //injected response proxy supporting multiple threads
private HttpServletResponse response;

@Path("/foos/{fooId}")
@PUT
@Consumes("application/json")
@Produces("application/json")
public Foo setFoo(@PathParam("fooID") final String fooID, final Foo foo)
{
  //TODO store foo in persistent storage
  if(itemDidNotExistBefore) //return 201 only if new object; TODO app-specific logic
  {
    response.setStatus(Response.Status.CREATED.getStatusCode());
  }
  return foo;  //TODO get latest foo from storage if needed
}

1 votes

Vous pouvez en fait combiner les approches : annoter la méthode avec @Produces et ne spécifiez pas le type de média dans la version finale. Response.ok et vous obtiendrez votre objet de retour correctement sérialisé par JAXB dans le type de média approprié à la requête. (Je viens d'essayer ceci avec une seule méthode qui pouvait retourner soit XML soit JSON : la méthode elle-même n'a pas besoin de mentionner l'un ou l'autre, sauf dans le champ @Produces annotation).

0 votes

Vous avez raison Garret. Mon exemple visait plutôt à illustrer l'importance de fournir un type de contenu. Nos approches sont similaires, mais l'idée d'utiliser un fichier MessageBodyWriter y Provider permet une négociation implicite du contenu, bien qu'il semble qu'il manque du code dans votre exemple. Voici une autre réponse que j'ai fournie et qui illustre cela : stackoverflow.com/questions/5161466/

8 votes

Je ne parviens pas à modifier le code d'état dans la section response.setStatus() . La seule façon d'envoyer par exemple un 404 Non trouvé est de définir le code d'état de la réponse response.setStatus(404) en puis fermer le flux de sortie response.getOutputStream().close() donc JAX-RS ne peut pas réinitialiser mon statut.

40voto

Nthalk Points 1260

Si vous aimez garder votre couche de ressources propre de Response alors je vous recommande d'utiliser @NameBinding et la liaison avec les implémentations de ContainerResponseFilter .

Voici l'essentiel de l'annotation :

package my.webservice.annotations.status;

import javax.ws.rs.NameBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface Status {
  int CREATED = 201;
  int value();
}

Voici l'essentiel du filtre :

package my.webservice.interceptors.status;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;

@Provider
public class StatusFilter implements ContainerResponseFilter {

  @Override
  public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException {
    if (containerResponseContext.getStatus() == 200) {
      for (Annotation annotation : containerResponseContext.getEntityAnnotations()) {
        if(annotation instanceof Status){
          containerResponseContext.setStatus(((Status) annotation).value());
          break;
        }
      }
    }
  }
}

Et puis la mise en œuvre sur votre ressource devient simplement :

package my.webservice.resources;

import my.webservice.annotations.status.StatusCreated;
import javax.ws.rs.*;

@Path("/my-resource-path")
public class MyResource{
  @POST
  @Status(Status.CREATED)
  public boolean create(){
    return true;
  }
}

0 votes

L'API reste propre, bonne réponse. Serait-il possible de paramétrer votre annotation comme @Status(code = 205), et que l'intercepteur remplace le code par ce que vous spécifiez ? Je pense que ça te donnerait une annotation pour remplacer les codes quand tu en as besoin.

0 votes

@user2800708, j'ai déjà fait cela pour mon code local, j'ai mis à jour la réponse comme vous l'avez suggéré.

0 votes

Bien, merci. Avec cela et quelques autres astuces, je suis maintenant capable de nettoyer les API REST dans mon code, de sorte qu'il se conforme à une simple interface Java sans aucune mention de REST ; c'est juste un autre mécanisme RMI.

5voto

kvista Points 3681

JAX-RS prend en charge les codes HTTP standard et personnalisés. Voir ResponseBuilder et ResponseStatus, par exemple :

http://jackson.codehaus.org/javadoc/jax-rs/1.0/javax/ws/rs/core/Response.ResponseBuilder.html#status%28javax.ws.rs.core.Response.Status%29

Gardez à l'esprit que les informations JSON concernent davantage les données associées à la ressource/application. Les codes HTTP concernent davantage l'état de l'opération CRUD demandée. (du moins, c'est ainsi que les choses sont censées se passer dans les systèmes REST).

0 votes

Le lien est cassé

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X