java - headers - JAX-RS-¿Cómo devolver el código de estado JSON y HTTP juntos?
response.status java (12)
Estoy escribiendo una aplicación web REST (NetBeans 6.9, JAX-RS, TopLink Essentials) y estoy tratando de devolver el código de estado JSON y HTTP. Tengo un código listo y en funcionamiento que devuelve JSON cuando se llama al método HTTP GET desde el cliente. Esencialmente:
@Path("get/id")
@GET
@Produces("application/json")
public M_機械 getMachineToUpdate(@PathParam("id") String id) {
// some code to return JSON ...
return myJson;
}
Pero también quiero devolver un código de estado HTTP (500, 200, 204, etc.) junto con los datos JSON.
Intenté usar HttpServletResponse
:
response.sendError("error message", 500);
Pero esto hizo que el navegador pensara que era un 500 "real", por lo que la página web de salida era una página de error HTTP 500 normal.
Quiero devolver un código de estado HTTP para que mi JavaScript del lado del cliente pueda manejar algo de lógica dependiendo de él (por ejemplo, para mostrar el código de error y el mensaje en una página HTML). ¿Es esto posible o no deberían usarse los códigos de estado HTTP para tal cosa?
Además, tenga en cuenta que, de forma predeterminada, Jersey anulará el cuerpo de la respuesta en caso de un código http 400 o más.
Para obtener su entidad especificada como cuerpo de respuesta, intente agregar el siguiente init-param a su Jersey en su archivo de configuración 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>
Aquí hay un ejemplo:
@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();
}
Echa un vistazo a la clase de Response .
Tenga en cuenta que siempre debe especificar un tipo de contenido, especialmente si está pasando varios tipos de contenido, pero si cada mensaje se representará como JSON, puede anotar el método con @Produces("application/json")
En caso de que desee cambiar el código de estado debido a una excepción, con JAX-RS 2.0 puede implementar un ExceptionMapper como este. Esto maneja este tipo de excepción para toda la aplicación.
@Provider
public class UnauthorizedExceptionMapper implements ExceptionMapper<EJBAccessException> {
@Override
public Response toResponse(EJBAccessException exception) {
return Response.status(Response.Status.UNAUTHORIZED.getStatusCode()).build();
}
}
Estoy usando jersey 2.0 con lectores y escritores de cuerpos de mensajes. Mi tipo de devolución de método era una entidad específica que también se usaba en la implementación del escritor del cuerpo del mensaje y estaba devolviendo el mismo pojo, un SkuListDTO. @GET @Consumes ({"application / xml", "application / json"}) @Produces ({"application / xml", "application / json"}) @Path ("/ skuResync")
public SkuResultListDTO getSkuData()
....
return SkuResultListDTO;
Todo lo que cambié fue esto, dejé la implementación del escritor solo y todavía funcionó.
public Response getSkuData()
...
return Response.status(Response.Status.FORBIDDEN).entity(dfCoreResultListDTO).build();
Existen varios casos de uso para configurar códigos de estado HTTP en un servicio web REST, y al menos uno no estaba suficientemente documentado en las respuestas existentes (es decir, cuando está utilizando la serialización JSON / XML automática y mágica con JAXB, y desea devolver un objeto para ser serializado, pero también un código de estado diferente al predeterminado 200).
Así que déjame intentar y enumerar los diferentes casos de uso y las soluciones para cada uno:
1. Código de error (500, 404, ...)
El caso de uso más común cuando desea devolver un código de estado diferente de 200 OK
es cuando se produce un error.
Por ejemplo:
- se solicita una entidad pero no existe (404)
- La solicitud es semánticamente incorrecta (400).
- el usuario no esta autorizado (401)
- Hay un problema con la conexión de la base de datos (500).
- etc.
a) Lanzar una excepción
En ese caso, creo que la forma más limpia de manejar el problema es lanzar una excepción. Esta excepción será manejada por un ExceptionMapper
, que traducirá la excepción en una respuesta con el código de error apropiado.
Puede usar el ExceptionMapper
predeterminado que viene preconfigurado con Jersey (y supongo que es lo mismo con otras implementaciones) y lanzar cualquiera de las subclases existentes de javax.ws.rs.WebApplicationException
. Estos son tipos de excepción predefinidos que se asignan previamente a diferentes códigos de error, por ejemplo:
- BadRequestException (400)
- InternalServerErrorException (500)
- NotFoundException (404)
Etc. Puedes encontrar la lista aquí: API
Alternativamente, puede definir sus propias excepciones personalizadas y clases de ExceptionMapper
, y agregar estos mapeadores a Jersey por medio de la anotación @Provider
( fuente de este ejemplo ):
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);
}
}
Proveedor :
@Provider
public class MyApplicationExceptionHandler implements ExceptionMapper<MyApplicationException>
{
@Override
public Response toResponse(MyApplicationException exception)
{
return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();
}
}
Nota: también puede escribir ExceptionMappers para los tipos de excepción existentes que utiliza.
b) Usar el generador de respuestas
Otra forma de establecer un código de estado es usar un generador de Response
para generar una respuesta con el código deseado.
En ese caso, el tipo de retorno de su método debe ser javax.ws.rs.core.Response
. Esto se describe en varias otras respuestas, como la respuesta aceptada de hisdrewness y se ve así:
@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. Éxito, pero no 200
Otro caso cuando desea establecer el estado de retorno es cuando la operación fue exitosa, pero desea devolver un código de éxito diferente a 200, junto con el contenido que devuelve en el cuerpo.
Un caso de uso frecuente es cuando crea una nueva entidad (solicitud POST
) y desea devolver información sobre esta nueva entidad o tal vez la propia entidad, junto con un código de estado 201 Created
.
Un enfoque es utilizar el objeto de respuesta tal como se describió anteriormente y establecer el cuerpo de la solicitud usted mismo. Sin embargo, al hacer esto, pierde la capacidad de usar la serialización automática a XML o JSON proporcionada por JAXB.
Este es el método original que devuelve un objeto de entidad que será serializado a JSON por 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;
}
Esto devolverá una representación JSON del usuario recién creado, pero el estado de retorno será 200, no 201.
Ahora el problema es que si quiero usar el generador de Response
para establecer el código de retorno, tengo que devolver un objeto de Response
en mi método. ¿Cómo puedo devolver el objeto User
a ser serializado?
a) Establecer el código en la respuesta del servlet.
Un enfoque para resolver esto es obtener un objeto de solicitud de servlet y establecer el código de respuesta manualmente nosotros mismos, como se demostró en la respuesta 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;
}
El método aún devuelve un objeto de entidad y el código de estado será 201.
Tenga en cuenta que para que funcione, tuve que limpiar la respuesta. Este es un desagradable resurgimiento del código de API de Servlet de bajo nivel en nuestro agradable recurso JAX_RS, y mucho peor, hace que los encabezados no se puedan modificar después de esto porque ya se enviaron por cable.
b) Usar el objeto de respuesta con la entidad.
La mejor solución, en ese caso, es usar el objeto Respuesta y configurar la entidad para que se serialice en este objeto de respuesta. Sería bueno hacer que el objeto de Respuesta sea genérico para indicar el tipo de entidad de carga útil en ese caso, pero actualmente no es el caso.
@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();
}
En ese caso, usamos el método creado de la clase de generador de respuesta para establecer el código de estado en 201. Pasamos el objeto de entidad (usuario) a la respuesta a través del método de entidad ().
El resultado es que el código HTTP es 401 como queríamos, y el cuerpo de la respuesta es exactamente el mismo JSON que teníamos antes cuando devolvimos el objeto Usuario. También agrega un encabezado de ubicación.
La clase Respuesta tiene un número de método de construcción para diferentes estados (stati?) Como:
Response.accepted () Response.ok () Response.noContent () Response.notAcceptable ()
NB: el objeto hateoas es una clase auxiliar que desarrollé para ayudar a generar recursos URI. Necesitará crear su propio mecanismo aquí;)
Eso es todo.
Espero que esta respuesta larga ayude a alguien :)
JAX-RS tiene soporte para códigos HTTP estándar / personalizados. Ver ResponseBuilder y ResponseStatus, por ejemplo:
Tenga en cuenta que la información JSON es más sobre los datos asociados con el recurso / aplicación. Los códigos HTTP son más sobre el estado de la operación CRUD que se solicita. (Al menos así es como se supone que debe ser en sistemas REST-ful)
La respuesta de hisdrewness funcionará, pero modifica todo el enfoque para permitir que un proveedor como Jackson + JAXB convierta automáticamente su objeto devuelto a algún formato de salida como JSON. Inspirado en una post Apache CXF (que utiliza una clase específica de CXF), he encontrado una manera de configurar el código de respuesta que debería funcionar en cualquier implementación de JAX-RS: inyecte un contexto HttpServletResponse y configure manualmente el código de respuesta. Por ejemplo, aquí es cómo establecer el código de respuesta en CREATED
cuando sea apropiado.
@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
}
Mejora: ¡ Después de encontrar otra answer relacionada, aprendí que uno puede inyectar HttpServletResponse
como una variable miembro, incluso para la clase de servicio de singleton (al menos en RESTEasy)! Este es un enfoque mucho mejor que contaminar la API con detalles de implementación. Se vería así:
@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
}
Me pareció muy útil construir también un mensaje json con código repetido, como este:
@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();
}
}
Mire el ejemplo aquí, ilustra mejor el problema y cómo se resuelve en la última versión (2.3.1) de Jersey.
https://jersey.java.net/documentation/latest/representations.html#d0e3586
Básicamente, implica definir una excepción personalizada y mantener el tipo de retorno como la entidad. Cuando hay un error, se lanza la excepción, de lo contrario, devuelve el POJO.
No estoy usando JAX-RS, pero tengo un escenario similar donde uso:
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
Si desea mantener su capa de recursos limpia de objetos de Response
, entonces le recomiendo que use @NameBinding
y enlace a las implementaciones de ContainerResponseFilter
.
Aquí está la carne de la anotación:
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();
}
Aquí está 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;
}
}
}
}
}
Y luego la implementación en su recurso simplemente se convierte en:
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;
}
}
Si su WS-RS necesita generar un error, ¿por qué no usar la excepción 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;
}