JAX-RS -如何返回JSON和HTTP状态码一起?

我正在编写一个REST web应用程序(NetBeans 6.9, JAX-RS, TopLink Essentials),并试图返回JSON 而且 HTTP状态码。我已经准备好了代码,并在从客户端调用HTTP GET方法时返回JSON。从本质上讲:

@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状态码不应该用于这样的事情?

554224 次浏览

我没有使用JAX-RS,但我有一个类似的场景,我使用:

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

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系统中是这样的)

这里有一个例子:

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

看一下响应类。

请注意,您应该始终指定一个内容类型,特别是当您传递多个内容类型时,但如果每个消息都将表示为JSON,则可以使用@Produces("application/json")注释该方法

他的答案是可行的,但它修改了整个方法,让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
}

如果您希望因为异常而更改状态代码,可以使用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();
}


}

如果您的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;
}

请看这里的例子,它最好地说明了这个问题,以及在Jersey的最新(2.3.1)版本中是如何解决的。

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

它基本上包括定义一个自定义Exception并将返回类型保留为实体。当出现错误时,抛出异常,否则,返回POJO。

在REST web服务中设置HTTP状态代码有几个用例,现有的答案中至少有一个没有充分记录(例如,当您使用JAXB使用自动神奇的JSON/XML序列化时,并且您希望返回一个要序列化的对象,但状态代码也不同于默认的200)。

所以让我试着列举不同的用例和每个用例的解决方案:

1. 错误码(500,404,…)

当你想返回一个不同于200 OK的状态码时,最常见的用例是发生错误时。

例如:

  • 请求一个实体,但它不存在(404)
  • 请求在语义上不正确(400)
  • 用户未被授权(401)
  • 数据库连接有问题(500)
  • 等。

a)抛出异常

在这种情况下,我认为处理问题的最干净的方法是抛出异常。此异常将由ExceptionMapper处理,它将异常转换为带有适当错误代码的响应。

你可以使用默认的ExceptionMapper,它预先配置了Jersey(我猜其他实现也是一样的),并抛出javax.ws.rs.WebApplicationException的任何现有子类。这些是预先定义的异常类型,被预先映射到不同的错误代码,例如:

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

等。你可以在这里找到列表:API

或者,你也可以定义自己的自定义异常和ExceptionMapper类,并通过@Provider注释(这个例子的来源)将这些映射器添加到Jersey:

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构建器

另一种设置状态代码的方法是使用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状态码。

一种方法是像上面描述的那样使用响应对象,并自己设置请求的主体。但是,这样做就失去了使用JAXB提供的自动序列化到XML或JSON的能力。

这是返回一个将被JAXB序列化为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)在servlet响应上设置代码

解决这个问题的一种方法是获取一个servlet请求对象,并手动设置响应代码,就像garrett 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;
}

该方法仍然返回一个实体对象,状态代码将是201。

注意,为了使它工作,我必须刷新响应。这是JAX_RS资源中低级Servlet API代码令人不快的重新出现,更糟糕的是,它导致在此之后头不可修改,因为它们已经通过网络发送了。

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

在这种情况下,我们使用Response构建器类的已创建方法来将状态代码设置为201。我们通过entity()方法将实体对象(用户)传递给响应。

结果是HTTP代码是我们想要的401,响应的主体是与我们之前返回User对象时完全相同的JSON。它还添加了一个位置标头。

Response类有许多用于不同状态的构建器方法(stati ?),例如:

< p > Response.accepted () Response.ok () Response.noContent () Response.notAcceptable () < / p >

注意:hateoas对象是一个助手类,我开发它来帮助生成资源uri。你需要想出你自己的机制;)

差不多就是这样。

我希望这个冗长的回答能帮助到一些人:)

如果你想让你的资源层没有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;
}
}

我发现用重复的代码构建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();
}
}

我使用jersey 2.0的消息体阅读器和作者。我有我的方法返回类型作为一个特定的实体,这也用于实现的消息体作者和我返回相同的pojo,一个SkuListDTO。 @ get @Consumes({“application / xml”、“application / json "}) 与@({“application / xml”、“application / json "}) @ path (" / skuResync”)< / p >

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

我所改变的是这个,我离开作家实现单独和它仍然工作。

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

另外,请注意,缺省情况下,如果http代码为400或更多,Jersey将覆盖响应体。

为了得到你指定的实体作为响应体,试着在你的web.xml配置文件的Jersey中添加以下初始化参数:

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

下面的代码适用于我。通过带注释的setter注入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);
}
}

使用微断面OpenAPI扩展Nthalk回答,可以使用@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)