JAX-RS – Come restituire insieme il codice di stato JSON e HTTP?

Sto scrivendo un’app Web REST (NetBeans 6.9, JAX-RS, TopLink Essentials) e sto cercando di restituire il codice di stato JSON e HTTP. Ho il codice pronto e funzionante che restituisce JSON quando viene chiamato il metodo HTTP GET dal client. Essenzialmente:

@Path("get/id") @GET @Produces("application/json") public M_機械 getMachineToUpdate(@PathParam("id") String id) { // some code to return JSON ... return myJson; } 

Ma voglio anche restituire un codice di stato HTTP (500, 200, 204, ecc.) Insieme ai dati JSON.

Ho provato a utilizzare HttpServletResponse :

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

Ma questo ha fatto pensare al browser che fosse un “vero” 500, quindi la pagina Web di output era una normale pagina di errore HTTP 500.

Voglio restituire un codice di stato HTTP in modo che il mio JavaScript lato client sia in grado di gestire una logica a seconda di esso (ad esempio, visualizzare il codice di errore e il messaggio su una pagina HTML). È ansible o i codici di stato HTTP non dovrebbero essere usati per tale cosa?

Ecco un esempio:

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

Dai un’occhiata alla class Response .

Nota che devi sempre specificare un tipo di contenuto, specialmente se stai passando più tipi di contenuti, ma se ogni messaggio sarà rappresentato come JSON, puoi semplicemente annotare il metodo con @Produces("application/json")

Esistono diversi casi d’uso per impostare i codici di stato HTTP in un servizio web REST e almeno uno non è stato sufficientemente documentato nelle risposte esistenti (ad esempio quando si utilizza la serializzazione JSON / XML auto-magica usando JAXB e si desidera restituire un object da serializzare, ma anche un codice di stato diverso da quello predefinito 200).

Lasciatemi provare e elencare i diversi casi d’uso e le soluzioni per ciascuno:

1. Codice di errore (500, 404, …)

Il caso d’uso più comune quando si desidera restituire un codice di stato diverso da 200 OK è quando si verifica un errore.

Per esempio:

  • un’ quadro è richiesta ma non esiste (404)
  • la richiesta è semanticamente errata (400)
  • l’utente non è autorizzato (401)
  • c’è un problema con la connessione al database (500)
  • eccetera..

a) Lanciare un’eccezione

In tal caso, penso che il modo più pulito per gestire il problema sia quello di lanciare un’eccezione. Questa eccezione verrà gestita da ExceptionMapper , che tradurrà l’eccezione in una risposta con il codice di errore appropriato.

Puoi usare l’ ExceptionMapper predefinito che viene preconfigurato con Jersey (e suppongo che sia lo stesso con altre implementazioni) e lanciare una qualsiasi delle sottoclassi esistenti di javax.ws.rs.WebApplicationException . Questi sono tipi di eccezioni predefinite che sono pre-mappate a diversi codici di errore, ad esempio:

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

Ecc. Puoi trovare l’elenco qui: API

In alternativa, è ansible definire le proprie eccezioni personalizzate e le classi ExceptionMapper e aggiungere questi mappatori a Jersey mediante l’annotazione @Provider ( fonte di questo esempio ):

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

Fornitore:

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

Nota: è ansible anche scrivere ExceptionMappers per i tipi di eccezioni esistenti che si utilizzano.

b) Utilizzare il generatore di risposte

Un altro modo per impostare un codice di stato consiste nell’utilizzare un generatore di Response per creare una risposta con il codice previsto.

In tal caso, il tipo di restituzione del metodo deve essere javax.ws.rs.core.Response . Questo è descritto in varie altre risposte, come la risposta accettata di hisdrewness e assomiglia a questo:

 @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. Successo, ma non 200

Un altro caso in cui si desidera impostare lo stato di ritorno è quando l’operazione ha avuto successo, ma si desidera restituire un codice di successo diverso da 200, insieme al contenuto restituito nel corpo.

Un caso di utilizzo frequente è quando crei una nuova quadro (richiesta POST ) e vuoi restituire informazioni su questa nuova quadro o forse sull’ quadro stessa, insieme a un 201 Created stato di codice 201 Created .

Un approccio consiste nell’utilizzare l’object risposta proprio come descritto sopra e impostare autonomamente il corpo della richiesta. Tuttavia, in questo modo perdi la possibilità di utilizzare la serializzazione automatica in XML o JSON fornita da JAXB.

Questo è il metodo originale che restituisce un object quadro che verrà serializzato su JSON da 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; } 

Ciò restituirà una rappresentazione JSON dell’utente appena creato, ma lo stato di ritorno sarà 200, non 201.

Ora il problema è che se voglio usare il builder Response per impostare il codice di ritorno, devo restituire un object Response nel mio metodo. Come posso ancora restituire l’object User da serializzare?

a) Impostare il codice sulla risposta della servlet

Un approccio per risolverlo è ottenere un object di richiesta servlet e impostare manualmente il codice di risposta, come dimostrato nella risposta di 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; } 

Il metodo restituisce ancora un object quadro e il codice di stato sarà 201.

Si noti che per farlo funzionare, ho dovuto scaricare la risposta. Questa è una spiacevole ripresa del codice API Servlet di basso livello nella nostra bella risorsa JAX_RS, e molto peggio, causa le intestazioni non modificabili dopo questo perché erano già state inviate sul filo.

b) Utilizzare l’object risposta con l’ quadro

La soluzione migliore, in questo caso, è utilizzare l’object Response e impostare l’entity framework da serializzare su questo object risposta. Sarebbe bello rendere generico l’object Response per indicare il tipo dell’ quadro payload in quel caso, ma non è il caso attuale.

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

In tal caso, utilizziamo il metodo creato della class del builder Response per impostare il codice di stato su 201. Passiamo l’object quadro (utente) alla risposta tramite il metodo entity ().

Il risultato è che il codice HTTP è 401 come volevamo, e il corpo della risposta è esattamente lo stesso JSON di prima, quando abbiamo appena restituito l’object User. Aggiunge anche un’intestazione di posizione.

La class Response ha un numero di metodi builder per diversi stati (stati?) Come:

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

NB: l’object hateoas è una class helper che ho sviluppato per aiutare a generare URI di risorse. Dovrai trovare il tuo meccanismo qui;)

Questo è tutto.

Spero che questa lunga risposta aiuti qualcuno 🙂

La risposta da parte di hisdrewn funzionerà, ma modifica l’intero approccio per consentire a un provider come Jackson + JAXB di convertire automaticamente l’object restituito in un formato di output come JSON. Ispirato a un post di Apache CXF (che utilizza una class specifica di CXF), ho trovato un modo per impostare il codice di risposta che dovrebbe funzionare in qualsiasi implementazione JAX-RS: iniettare un contesto HttpServletResponse e impostare manualmente il codice di risposta. Ad esempio, ecco come impostare il codice di risposta su CREATED quando appropriato.

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

Miglioramento: dopo aver trovato un’altra risposta correlata, ho imparato che si può iniettare HttpServletResponse come variabile membro, anche per la class di servizio singleton (almeno in RESTEasy) !! Questo è un approccio molto migliore rispetto all’inquinamento dell’API con i dettagli di implementazione. Sarebbe come questo:

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

Se si desidera mantenere pulito il livello delle risorse degli oggetti Response , si consiglia di utilizzare @NameBinding e il binding alle implementazioni di ContainerResponseFilter .

Ecco la carne dell’annotazione:

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

Ecco la carne del filtro:

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

E poi l’implementazione sulla tua risorsa diventa semplicemente:

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

Nel caso in cui si desideri modificare il codice di stato a causa di un’eccezione, con JAX-RS 2.0 è ansible implementare un ExceptionMapper come questo. Questo gestisce questo tipo di eccezione per l’intera app.

 @Provider public class UnauthorizedExceptionMapper implements ExceptionMapper { @Override public Response toResponse(EJBAccessException exception) { return Response.status(Response.Status.UNAUTHORIZED.getStatusCode()).build(); } } 

JAX-RS supporta i codici HTTP standard / personalizzati. Vedere ResponseBuilder e ResponseStatus, ad esempio:

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

Tenere presente che le informazioni JSON riguardano maggiormente i dati associati alla risorsa / applicazione. I codici HTTP riguardano maggiormente lo stato dell’operazione CRUD richiesta. (almeno questo è come dovrebbe essere nei sistemi REST-ful)

Se il tuo WS-RS ha bisogno di generare un errore, perché non usare solo 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; } 

Non sto usando JAX-RS, ma ho uno scenario simile in cui utilizzo:

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

Si prega di guardare l’esempio qui, illustra meglio il problema e come è risolto nell’ultima versione (2.3.1) di Jersey.

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

Fondamentalmente implica la definizione di un’eccezione personalizzata e il mantenimento del tipo di ritorno come entity framework. Quando si verifica un errore, viene generata l’eccezione, altrimenti si restituisce il POJO.

Ho trovato molto utile creare anche un messaggio JSON con codice ripetuto, come questo:

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

Inoltre, noterai che, per impostazione predefinita, Jersey sostituirà il corpo della risposta in caso di un codice http 400 o superiore.

Per ottenere l’ quadro specificata come corpo della risposta, prova ad aggiungere il seguente init-param al tuo Jersey nel tuo file di configurazione web.xml:

    jersey.config.server.response.setStatusOverSendError true  

Sto usando la maglia 2.0 con i lettori del corpo del messaggio e gli scrittori. Ho avuto il mio metodo return type come entity framework specifica che è stata utilizzata anche nell’implementazione del body writer del messaggio e stavo restituendo lo stesso pojo, uno SkuListDTO. @GET @Consumes ({“application / xml”, “application / json”}) @Produces ({“application / xml”, “application / json”}) @Path (“/ skuResync”)

 public SkuResultListDTO getSkuData() .... return SkuResultListDTO; 

tutto ciò che ho cambiato è stato questo, ho lasciato l’implementazione dell’autore da solo e ha comunque funzionato.

 public Response getSkuData() ... return Response.status(Response.Status.FORBIDDEN).entity(dfCoreResultListDTO).build();