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.