JAX-RS - Как вернуть код состояния JSON и HTTP вместе? - PullRequest
232 голосов
/ 14 января 2011

Я пишу веб-приложение REST (NetBeans 6.9, JAX-RS, TopLink Essentials) и пытаюсь вернуть код состояния HTTP 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 для такой вещи?

Ответы [ 13 ]

323 голосов
/ 14 января 2011

Вот пример:

@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")

178 голосов
/ 04 апреля 2014

Существует несколько вариантов использования для установки кодов состояния 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)

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

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

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.Это описано в различных других ответах, таких как принятый ответ его самого, и выглядит так:

@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 запрос) и хотите вернуть информацию об этой новой сущности или, возможно, о самой сущности вместе с 201 Created кодом состояния.

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

Это оригинальный метод, возвращающий объект сущности, который будет сериализован в JSON 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;
}

Это вернет 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, и, что еще хуже, после этого заголовки становятся неизменяемыми, поскольку они уже были отправлены по проводам.объект ответа с сущностью

Лучшее решение в этом случае состоит в том, чтобы использовать объект 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();
}

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

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

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

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

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

Вот и все.

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

69 голосов
/ 09 апреля 2012

Ответ от hisdrewness будет работать, но он изменяет весь подход, позволяя провайдеру, такому как Jackson + JAXB, автоматически преобразовывать возвращаемый объект в некоторый формат вывода, такой как JSON.Вдохновленный Apache CXF post (который использует специфичный для 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
}
32 голосов
/ 06 сентября 2014

Если вы хотите, чтобы ваш уровень ресурсов был чист от 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;
  }
}
6 голосов
/ 19 мая 2013

Если вы хотите изменить код состояния из-за исключения, в 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();
    }

}
5 голосов
/ 19 июня 2013

Если вашему 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;
}
5 голосов
/ 14 января 2011

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)

4 голосов
/ 28 августа 2015

Я нашел очень полезным также создать сообщение 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();
    }
}
4 голосов
/ 15 октября 2013

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

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

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

3 голосов
/ 14 января 2011

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

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