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


248

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


2
Какая разница между 500 вы хотите (нереально? :)) и реальными 500?
бритва

@razor Здесь реальные 500 означают страницу с ошибкой HTML вместо ответа JSON
Nupur

Веб-браузер не был разработан для работы с JSON, но для HTML-страниц, поэтому, если вы ответите 500 (и даже некоторым телом сообщения), браузер может показать вам просто сообщение об ошибке (действительно зависит от реализации браузера), просто потому, что это полезно обычный пользователь.
бритва

Ответы:


347

Вот пример:

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


12
Это работает, но что мне не нравится в возвращаемом значении Response, так это то, что, по моему мнению, он загрязняет ваш код, особенно в отношении любого клиента, пытающегося его использовать. Если вы предоставляете интерфейс, возвращающий ответ третьей стороне, он не знает, какой тип вы действительно возвращаете. Spring делает это более понятным с помощью аннотации, что очень полезно, если вы всегда возвращаете код состояния (т. Е. HTTP 204)
Гвидо

19
Создание общего класса (Response <T>) было бы интересным улучшением для jax-rs, чтобы иметь преимущества обеих альтернатив.
Гвидо

41
Нет необходимости каким-либо образом преобразовывать сущность в json. Вы можете выполнять return Response.status(Response.Status.Forbidden).entity(myPOJO).build();Работы, как если бы вы это делали return myPOJO;, но с дополнительной настройкой кода HTTP-статуса.
Кратенко

1
Я думаю, что разделение бизнес-логики на отдельный класс обслуживания работает хорошо. Конечная точка использует Response в качестве возвращаемого типа, а ее методы - это, в основном, просто вызовы сервисных методов плюс аннотации пути и параметра. Он четко отделяет логику от отображения типа URL / контента (где, так сказать, резина отправляется в путь).
Стейн де Витт

на самом деле, можно просто вернуть объект, не являющийся переносом, в Response.
SES

192

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

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

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

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

Например:

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

а) бросить исключение

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

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

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

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

@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объект для сериализации?

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

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

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

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

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

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

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

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

Вот и все.

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


Интересно, есть ли чистый способ вернуть сам объект данных вместо ответа. Это flushдействительно грязно.
Алик Эльзин-килака

1
Просто моя любимая мозоль: 401 не означает, что пользователь не авторизован. Это означает, что клиент не авторизован, потому что сервер не знает, кто вы. Если зарегистрированный / иначе признанный пользователь не имеет права выполнять определенное действие, правильный код ответа - 403 Запрещено.
Demonblack

69

Ответ от 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
}

На самом деле вы можете комбинировать подходы: аннотировать метод @Produces, и не указывать тип носителя в финале Response.ok, и вы получите правильно возвращаемый JAXB-сериализованный объект в соответствующий тип носителя, соответствующий запросу. (Я только что попробовал это с помощью одного метода, который мог бы возвращать либо XML, либо JSON: сам метод не должен упоминаться, кроме как в @Producesаннотации.)
Royston Shufflebotham

Вы правы, Гаррет. Мой пример был скорее иллюстрацией акцента предоставления типа контента. Наши подходы похожи, но идея использования MessageBodyWriterи Providerдопускает неявное согласование содержимого, хотя кажется, что в вашем примере отсутствует некоторый код. Вот еще один ответ, который я предоставил, который иллюстрирует это: stackoverflow.com/questions/5161466/…
hisdrewness

8
Я не могу переопределить код состояния в response.setStatus(). Например, единственный способ отправить ответ 404 Not Found - это установить код состояния ответа response.setStatus(404)en, а затем закрыть выходной поток, response.getOutputStream().close()чтобы JAX-RS не мог сбросить мой статус.
Роб Юурлинк

2
Я смог использовать этот подход для установки кода 201, но мне пришлось добавить блок try-catch response.flushBuffer(), чтобы избежать переопределения моего кода ответа каркасом. Не очень чисто.
Пьер Генри

1
@RobJuurlink, если вы хотите специально вернуть a 404 Not Found, может быть проще просто использовать throw new NotFoundException().
Гаррет Уилсон

34

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

Поддерживает API в чистоте, хороший ответ. Можно ли параметризовать вашу аннотацию, например @Status (code = 205), и перехватчик заменит код тем, что вы укажете? Я думаю, что это в основном даст вам аннотацию для переопределения кодов, когда вам нужно.
user2800708

@ user2800708, я уже сделал это для моего локального кода, обновил ответ, как вы предложили.
Nthalk

Здорово спасибо С этим и несколькими другими приемами я теперь в основном могу очистить API REST в моем коде, чтобы он соответствовал простому интерфейсу Java без упоминания о REST; это просто еще один механизм RMI.
user2800708

6
Вместо создания цикла для аннотаций в StatusFilter вы можете пометить фильтр с помощью @ Status в дополнение к @ Provider. Тогда фильтр будет вызываться только для ресурсов, помеченных @ Status. Это цель @ NameBinding
треворизм

1
Хорошая выноска @trevorism. Есть один не очень приятный побочный эффект аннотирования StatusFilterс помощью @Status: вам нужно либо указать значение по умолчанию для valueполя аннотации , либо объявить его при аннотировании класса (например:) @Status(200). Это явно не идеально.
Фил

6

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

Если вашему 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;
}

4
По моему мнению, WebApplicationExceptions не подходят для ошибок на стороне клиента, потому что они генерируют большие следы стека. Ошибки клиента не должны создавать следы стека на стороне сервера и загрязнять ведение журнала им.
Роб Джурлинк

5

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)


ссылка сломана
Umpa

5

Я нашел очень полезным также создать сообщение 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

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

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

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


Я хотел бы добавить, что интересным примером является тот, где они определяют свой собственный класс исключений и создают Responseв нем. Просто найдите CustomNotFoundExceptionкласс и, возможно, скопируйте его в свой пост.
JBert

Я использую этот подход для ошибок, и мне это нравится. Но это не применимо к кодам успеха (отличным от 200), таким как «201 создан».
Пьер Генри

3

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

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

Это делает для меня использование Spring MVC, но есть простой способ узнать!
Caps

1

Также обратите внимание, что по умолчанию Джерси переопределяет тело ответа в случае 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>

0

Следующий код работал для меня. Внедрение 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);
    }
}

0

Развивая ответ на Nthalk с микропрофилем OpenAPI можно выровнять код возврата с документацией , используя @APIResponse аннотацию.

Это позволяет пометить метод JAX-RS как

@GET
@APIResponse(responseCode = "204")
public Resource getResource(ResourceRequest request) 

Вы можете проанализировать эту стандартизированную аннотацию с помощью ContainerResponseFilter.

@Provider
public class StatusFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
        if (responseContext.getStatus() == 200) {
            for (final var annotation : responseContext.getEntityAnnotations()) {
                if (annotation instanceof APIResponse response) {
                    final var rawCode = response.responseCode();
                    final var statusCode = Integer.parseInt(rawCode);

                    responseContext.setStatus(statusCode);
                }
            }
        }
    }

}

Предупреждение возникает, когда вы помещаете несколько аннотаций в ваш метод, например

@APIResponse(responseCode = "201", description = "first use case")
@APIResponse(responseCode = "204", description = "because you can")
public Resource getResource(ResourceRequest request) 

-1

Я использую Джерси 2.0 с читателями и авторами тел сообщений. У меня был метод возврата типа метода в качестве особой сущности, которая также использовалась в реализации средства записи тела сообщения, и я возвращал то же pojo, SkuListDTO. @GET @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();
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.