web-dev-qa-db-fra.com

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

J'écris une application Web REST (NetBeans 6.9, JAX-RS, TopLink Essentials) et tente de renvoyer le code JSON et Code d'état HTTP. J'ai du code prêt et fonctionnel qui renvoie JSON lorsque la méthode HTTP GET est appelée à partir du 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 souhaite également 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 amené le navigateur à penser qu’il s’agissait d’un "vrai" 500, de sorte que la page Web en sortie était une page d’erreur HTTP 500 classique.

Je souhaite renvoyer un code d'état HTTP afin que mon JavaScript côté client puisse gérer une logique en fonction (par exemple, afficher le code d'erreur et le message sur une page HTML). Est-ce possible ou ne faut-il pas utiliser les codes d'état HTTP?

233
masato-san

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 classe Réponse .

Notez que vous devez toujours spécifier un type de contenu, en particulier 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").

326
hisdrewness

Il existe plusieurs cas d'utilisation pour la définition de codes d'état HTTP dans un service Web REST, et au moins un d'entre eux n'a pas été suffisamment documenté dans les réponses existantes (c'est-à-dire lorsque vous utilisez la sérialisation JSON/XML automatique avec JAXB, et vous voulez renvoyer un objet à sérialiser, mais aussi un code d’état différent de celui par défaut 200).

Alors laissez-moi essayer d’énumérer les différents cas d’utilisation et les solutions pour chacun:

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

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

Par exemple:

  • une entité est demandée mais n'existe pas (404)
  • la requête 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 le moyen le plus propre de gérer le problème est de lever 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 la valeur par défaut ExceptionMapper qui est préconfigurée avec Jersey (et je suppose que c'est la même chose avec d'autres implémentations) et lancer l'une des sous-classes existantes de javax.ws.rs.WebApplicationException. Ce sont des types d'exceptions prédéfinis qui sont pré-mappés sur différents codes d'erreur, par exemple:

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

Etc. Vous pouvez trouver la liste ici: API

Vous pouvez également définir vos propres exceptions et classes ExceptionMapper personnalisées et ajouter ces mappeurs à Jersey par la moyenne de l'annotation @Provider ( 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);
    }
}

Fournisseur:

    @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 ExceptionMappers pour les types d'exception existants que vous utilisez.

b) Utiliser le générateur de réponse

Un autre moyen de définir un code d'état consiste à utiliser un générateur Response pour générer une réponse avec le code souhaité.

Dans ce cas, le type de retour de votre méthode doit être javax.ws.rs.core.Response. Ceci est décrit dans diverses autres réponses telles que la réponse acceptée de hisdrewness et se présente comme suit:

@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. Succès, mais pas 200

Un autre cas où vous souhaitez définir le statut de retour est le moment où l'opération a abouti, mais vous souhaitez renvoyer un code de réussite différent de 200, avec le contenu que vous renvoyez dans le corps.

Un cas d'utilisation fréquent est lorsque vous créez une nouvelle entité (demande POST) et souhaitez renvoyer des informations sur cette nouvelle entité ou peut-être l'entité elle-même, avec un code de statut 201 Created.

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

Il s'agit de la méthode d'origine renvoyant 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 le statut de retour sera 200, pas 201.

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

a) Définissez le code sur la réponse du servlet

Une solution pour résoudre ce problème consiste à obtenir un objet de requête servlet et à définir nous-mêmes le code de réponse manuellement, 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 retourne toujours un objet entité et le code de statut sera 201.

Notez que pour que cela fonctionne, je devais vider la réponse. Il s'agit d'une résurgence désagréable du code API de servlet de bas niveau dans notre ressource JAX_RS de Nice, et pire encore, les en-têtes ne sont plus modifiables car ils ont déjà été envoyés sur le réseau.

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

La meilleure solution, dans ce cas, consiste à utiliser l'objet Response et à définir l'entité à sérialiser sur cet objet Response. Il serait bien de rendre l’objet Response générique pour indiquer le type de l’entité de charge utile 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 créée de la classe de générateur Réponse afin de définir le code d'état sur 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 souhaitions et que le corps de la réponse est exactement le même JSON que celui que nous avions auparavant lorsque nous venons de renvoyer l'objet User. Il ajoute également un en-tête d'emplacement.

La classe Response possède un certain nombre de méthodes de générateur pour différents statuts (stati?), Tels que:

Response.accepted () Response.ok () Response.noContent () Response.notAcceptable ()

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

C'est à peu près ça.

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

179
Pierre Henry

La réponse donnée par hisdrewness fonctionnera, mais elle modifie l’ensemble de l’approche permettant à un fournisseur tel que Jackson + JAXB de convertir automatiquement l’objet renvoyé dans un format de sortie tel que JSON. Inspiré par un Apache CXF post (qui utilise une classe spécifique à CXF), j'ai trouvé un moyen de définir le code de réponse qui devrait fonctionner dans toute implémentation JAX-RS: injectez un contexte HttpServletResponse et définissez manuellement le code de réponse. Par exemple, voici comment définir le code de réponse sur 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é un autre réponse , j'ai appris qu'il est possible d'injecter la HttpServletResponse en tant que variable membre, même pour la classe de service singleton (au moins dans RESTEasy) !! C'est une approche bien meilleure 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
}
69
Garret Wilson

Si vous souhaitez que votre couche de ressources reste propre d'objets Response, il est recommandé d'utiliser @NameBinding et d'associer des implémentations de ContainerResponseFilter.

Voici la viande 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 la viande 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 alors l'implémentation 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;
  }
}
32
Nthalk

Si vous souhaitez modifier le code d'état à cause d'une exception, JAX-RS 2.0 vous permet d'implémenter un ExceptionMapper comme celui-ci. Cela gère ce type d'exception pour l'ensemble de l'application.

@Provider
public class UnauthorizedExceptionMapper implements ExceptionMapper<EJBAccessException> {

    @Override
    public Response toResponse(EJBAccessException exception) {
        return Response.status(Response.Status.UNAUTHORIZED.getStatusCode()).build();
    }

}
6
ercalamar

Si votre WS-RS doit générer une erreur, pourquoi ne pas simplement utiliser WebApplicationException?

@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Path("{id}")
public MyEntity getFoo(@PathParam("id") long id,  @QueryParam("lang")long idLanguage) {

if (idLanguage== 0){
    // No URL parameter idLanguage was sent
    ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST);
    builder.entity("Missing idLanguage parameter on request");
    Response response = builder.build();
    throw new WebApplicationException(response);
    }
... //other stuff to return my entity
return myEntity;
}
5
Ariel

JAX-RS prend en charge les codes HTTP standard/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

N'oubliez pas que les informations JSON concernent davantage les données associées à la ressource/l'application. Les codes HTTP concernent davantage le statut de l'opération CRUD demandée. (du moins c'est ce qui est supposé être dans les systèmes REST-ful)

5
kvista

J'ai trouvé très utile de construire aussi un message JSON avec du code répété, comme ceci:

@POST
@Consumes("application/json")
@Produces("application/json")
public Response authUser(JsonObject authData) {
    String email = authData.getString("email");
    String password = authData.getString("password");
    JSONObject json = new JSONObject();
    if (email.equalsIgnoreCase(user.getEmail()) && password.equalsIgnoreCase(user.getPassword())) {
        json.put("status", "success");
        json.put("code", Response.Status.OK.getStatusCode());
        json.put("message", "User " + authData.getString("email") + " authenticated.");
        return Response.ok(json.toString()).build();
    } else {
        json.put("status", "error");
        json.put("code", Response.Status.NOT_FOUND.getStatusCode());
        json.put("message", "User " + authData.getString("email") + " not found.");
        return Response.status(Response.Status.NOT_FOUND).entity(json.toString()).build();
    }
}
4
Array

Veuillez regarder l'exemple ci-dessous, il illustre le mieux le problème et comment il est résolu dans la dernière version (2.3.1) de Jersey.

https://jersey.Java.net/documentation/latest/representations.html#d0e3586

Cela implique essentiellement de définir une exception personnalisée et de conserver le type de retour en tant qu'entité. En cas d'erreur, l'exception est levée, sinon vous retournez le POJO.

4
annouk

Je n'utilise pas JAX-RS, mais j'ai un scénario similaire dans lequel j'utilise:

response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
3
Caps

En outre, notez que par défaut, Jersey remplacera le corps de la réponse dans le cas d’un code http 400 ou supérieur.

Pour obtenir votre entité spécifiée en tant que corps de la réponse, essayez d’ajouter le paramètre init-param suivant à votre maillot dans votre fichier de configuration web.xml:

    <init-param>
        <!-- used to overwrite default 4xx state pages -->
        <param-name>jersey.config.server.response.setStatusOverSendError</param-name>
        <param-value>true</param-value>
    </init-param>
1
Maxime T