JAX-RS - Как вернуть код состояния JSON и HTTP?

Я пишу веб-приложение REST (NetBeans 6.9, JAX-RS, TopLink Essentials) и пытаюсь вернуть код состояния JSON и. У меня есть готовый код и работающий, который возвращает JSON при вызове метода HTTP GET от клиента. По существу:

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

    // some code to return JSON ...

    return myJson;
}

Но я также хочу вернуть код состояния HTTP (500, 200, 204 и т.д.) вместе с данными JSON.

Я попытался использовать HttpServletResponse:

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

Но это заставило браузер считать его "реальным" 500, поэтому выходная веб-страница была обычной страницей ошибок HTTP 500.

Я хочу вернуть код состояния HTTP, чтобы мой клиентский JavaScript мог обрабатывать некоторую логику в зависимости от него (например, отображать код ошибки и сообщение на странице HTML). Возможно ли это или должны ли коды состояния HTTP не использоваться для такой вещи?

Ответ 1

Вот пример:

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

Взгляните на класс Response.

Обратите внимание, что вы всегда должны указывать тип содержимого, особенно если вы передаете несколько типов содержимого, но если каждое сообщение будет представлено как JSON, вы можете просто аннотировать метод с помощью @Produces("application/json")

Ответ 2

Существует несколько вариантов использования кодов состояния HTTP в веб-службе REST, и по крайней мере один из них недостаточно документирован в существующих ответах (т.е. когда вы используете автоматическую магическую сериализацию JSON/XML с помощью JAXB, и вы хотите для возврата объекта, который будет сериализован, а также код состояния, отличный от значения по умолчанию 200).

Итак, позвольте мне попробовать и перечислить различные варианты использования и решения для каждого из них:

1. Код ошибки (500, 404,...)

Самый распространенный вариант использования, когда вы хотите вернуть код состояния, отличный от 200 OK, - это когда возникает ошибка.

Например:

  • запрашивается сущность, но она не существует (404)
  • запрос семантически неверен (400)
  • пользователь не авторизовался (401)
  • существует проблема с соединением с базой данных (500)
  • и т.д..

a) Выбросить исключение

В этом случае я считаю, что самый чистый способ справиться с проблемой - это выбросить исключение. Это исключение будет обрабатываться ExceptionMapper, которое переведет исключение в ответ с соответствующим кодом ошибки.

Вы можете использовать стандартный ExceptionMapper, который предварительно настроен с помощью Джерси (и я думаю, что он аналогичен другим реализациям) и выкинуть любой из существующих подклассов javax.ws.rs.WebApplicationException. Это предварительно определенные типы исключений, которые предварительно сопоставляются с разными кодами ошибок, например:

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

Etc. Вы можете найти список здесь: API

В качестве альтернативы вы можете определить свои собственные пользовательские исключения и классы ExceptionMapper и добавить эти картографы в Джерси с помощью аннотации @Provider (source этого примера):

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

Поставщик:

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

Примечание. Вы также можете написать ExceptionMappers для существующих типов исключений, которые вы используете.

b) Используйте построитель ответов

Другой способ установить код состояния - использовать конструктор Response для построения ответа с заданным кодом.

В этом случае возвращаемый тип метода должен быть javax.ws.rs.core.Response. Это описано в различных других ответах, таких как принятый ответ hisdreessess и выглядит следующим образом:

@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. Успех, но не 200

Другой случай, когда вы хотите установить статус возврата, - это когда операция была успешной, но вы хотите вернуть код успеха, отличный от 200, вместе с содержимым, которое вы возвращаете в теле.

Частый случай использования - это когда вы создаете новый объект (POST request) и хотите вернуть информацию об этом новом сущности или, возможно, самом объекте вместе с кодом состояния 201 Created.

Один из подходов состоит в том, чтобы использовать объект ответа так же, как описано выше, и самостоятельно настроить тело запроса. Однако при этом вы теряете возможность использовать автоматическую сериализацию для XML или JSON, предоставляемую JAXB.

Это оригинальный метод, возвращающий объект объекта, который JSON будет сериализован JSON:

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

Это вернет представление JSON только что созданного пользователя, но статус возврата будет 200, а не 201.

Теперь проблема в том, хочу ли я использовать конструктор Response для установки кода возврата, я должен вернуть объект Response в свой метод. Как я все еще возвращаю объект User для сериализации?

a) Установите код в ответе сервлета

Один из подходов к решению этого вопроса - получить объект запроса сервлета и самостоятельно задать код ответа, как показано в ответе Гаретта Уилсона:

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

Метод все еще возвращает объект объекта, а код состояния - 201.

Обратите внимание, что для его работы мне пришлось сбросить ответ. Это неприятное возрождение кода API сервлета низкого уровня в нашем хорошем ресурсе JAX_RS, и, что еще хуже, это приводит к тому, что заголовки не поддаются изменению после этого, потому что они уже отправлены на провод.

b) Использовать объект ответа с сущностью

Лучшим решением в этом случае является использование объекта Response и установка сущности, которая будет сериализована в этом объекте ответа. Было бы неплохо сделать объект Response общим, чтобы указать тип объекта полезной нагрузки в этом случае, но в настоящее время это не так.

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

В этом случае мы используем созданный метод класса Builder-ответчика, чтобы установить код состояния 201. Мы передаем объект сущности (пользователя) в ответ через метод entity().

В результате код HTTP составляет 401, как мы хотели, и тело ответа является тем же самым JSON, что и раньше, когда мы только что вернули объект User. Он также добавляет заголовок местоположения.

Класс Response имеет несколько методов построения для разных статусов (stati?), таких как:

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

NB: объект hateoas является вспомогательным классом, который я разработал, чтобы помочь генерировать URI ресурсов. Вам нужно будет придумать свой механизм здесь;)

Что об этом.

Я надеюсь, что этот длительный ответ поможет кому-то:)

Ответ 3

Ответ от hisdrewness будет работать, но он изменяет весь подход, позволяя провайдеру, такому как Jackson + JAXB, автоматически конвертировать возвращаемый объект в некоторый формат вывода, такой как JSON. Вдохновленный публикацией Apache CXF (которая использует класс, специфичный для CXF), я нашел один способ установить код ответа, который должен работать в любой реализации JAX-RS: внедрить контекст HttpServletResponse и вручную установить код ответа. Например, вот как установить код ответа на CREATED когда это необходимо.

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

Улучшение: Найдя другой связанный ответ, я узнал, что можно ввести HttpServletResponse как переменную-член, даже для одноэлементного класса обслуживания (по крайней мере, в RESTEasy) !! Это гораздо лучший подход, чем загрязнение API деталями реализации. Это будет выглядеть так:

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

Ответ 4

Если вы хотите, чтобы ваш уровень ресурсов был очищен от объектов Response, я рекомендую использовать @NameBinding и привязку к реализациям ContainerResponseFilter.

Здесь мясо аннотации:

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

Здесь мясо фильтра:

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

И тогда реализация на вашем ресурсе просто станет:

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

Ответ 5

Если вы хотите изменить код состояния из-за исключения, с JAX-RS 2.0 вы можете реализовать ExceptionMapper, как это. Это обрабатывает этот вид исключения для всего приложения.

@Provider
public class UnauthorizedExceptionMapper implements ExceptionMapper<EJBAccessException> {

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

}

Ответ 6

JAX-RS поддерживает стандартные/пользовательские HTTP-коды. См. ResponseBuilder и ResponseStatus, например:

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

Имейте в виду, что информация JSON больше связана с данными, связанными с ресурсом/приложением. Коды HTTP больше относятся к статусу запрашиваемой операции CRUD. (по крайней мере, так оно и должно быть в REST-ful системах)

Ответ 7

Если ваш WS-RS нуждается в возникновении ошибки, почему бы просто не использовать исключение 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;
}

Ответ 8

Я не использую JAX-RS, но у меня есть аналогичный сценарий, в котором я использую:

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

Ответ 9

Пожалуйста, посмотрите здесь пример, это лучше всего иллюстрирует проблему и как она решена в последней версии (2.3.1) Джерси.

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

В основном это связано с определением пользовательского исключения и сохранением возвращаемого типа в качестве объекта. При возникновении ошибки генерируется исключение, иначе вы возвращаете POJO.

Ответ 10

Мне было очень полезно создавать также json-сообщение с повторным кодом, например:

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

Ответ 11

Также обратите внимание, что по умолчанию Джерси переопределит тело ответа в случае http-кода 400 или более.

Чтобы получить указанный объект в качестве тела ответа, попробуйте добавить следующий файл init-param в свой Джерси в файле конфигурации 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>

Ответ 12

Следующий код работал для меня. Внедрение messageContext через аннотированный установщик и установка кода состояния в моем методе "add".

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;

import org.apache.cxf.jaxrs.ext.MessageContext;

public class FlightReservationService {

    MessageContext messageContext;

    private final Map<Long, FlightReservation> flightReservations = new HashMap<>();

    @Context
    public void setMessageContext(MessageContext messageContext) {
        this.messageContext = messageContext;
    }

    @Override
    public Collection<FlightReservation> list() {
        return flightReservations.values();
    }

    @Path("/{id}")
    @Produces("application/json")
    @GET
    public FlightReservation get(Long id) {
        return flightReservations.get(id);
    }

    @Path("/")
    @Consumes("application/json")
    @Produces("application/json")
    @POST
    public void add(FlightReservation booking) {
        messageContext.getHttpServletResponse().setStatus(Response.Status.CREATED.getStatusCode());
        flightReservations.put(booking.getId(), booking);
    }

    @Path("/")
    @Consumes("application/json")
    @PUT
    public void update(FlightReservation booking) {
        flightReservations.remove(booking.getId());
        flightReservations.put(booking.getId(), booking);
    }

    @Path("/{id}")
    @DELETE
    public void remove(Long id) {
        flightReservations.remove(id);
    }
}

Ответ 13

Я использую jersey 2.0 с читателями и авторами сообщений. У меня был тип возвращаемого метода как конкретный объект, который также использовался в реализации автора сообщения сообщения, и я возвращал то же самое pojo, SkuListDTO.   @ПОЛУЧИТЬ   @Consumes ({ "application/xml", "application/json" })   @Produces ({ "application/xml", "application/json" })   @Path ( "/skuResync" )

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

Все, что я изменил, это то, что я оставил реализацию сценария в одиночку, и он все еще работал.

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