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

mas*_*san 237 java rest jax-rs http-status-codes

我正在编写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;
}
Run Code Online (Sandbox Code Playgroud)

但是,我想用JSON数据一起返回的HTTP状态代码(500,200,204,等).

我试着用HttpServletResponse:

response.sendError("error message", 500);
Run Code Online (Sandbox Code Playgroud)

但这使得浏览器认为它是"真正的"500,因此输出网页是一个常规的HTTP 500错误页面.

我想返回一个HTTP状态代码,以便我的客户端JavaScript可以根据它处理一些逻辑(例如在HTML页面上显示错误代码和消息).这是可能的还是HTTP状态代码不能用于此类事情?

his*_*ess 330

这是一个例子:

@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();
}
Run Code Online (Sandbox Code Playgroud)

看一下Response类.

请注意,您应始终指定内容类型,尤其是在传递多种内容类型的情况下,但如果每条消息都将表示为JSON,则只需注释该方法即可 @Produces("application/json")

  • 不需要以某种方式将实体转换为json.你可以`返回Response.status(Response.Status.Forbidden).entity(myPOJO).build();`就像你'返回myPOJO;`一样工作,但是需要额外设置HTTP-Status代码. (37认同)
  • 使该类通用(Response <T>)对jax-rs来说是一个有趣的改进,具有两种替代方案的优点. (18认同)
  • 它的工作原理,但我不喜欢Response返回值,在我看来,它会污染你的代码,特别是对任何试图使用它的客户端.如果您提供的界面将响应返回给第三方,则他不知道您真正返回的是什么类型.Spring使用注释更清晰,如果总是返回状态代码(即HTTP 204),则非常有用 (9认同)

Pie*_*nry 183

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

因此,让我尝试列举不同的用例和每个用例的解决方案:

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

想要返回与200 OK发生错误时不同的状态代码时最常见的用例.

例如:

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

a)抛出异常

在这种情况下,我认为处理问题的最简洁方法是抛出异常.此异常将由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);
    }
}
Run Code Online (Sandbox Code Playgroud)

提供者:

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

注意:您还可以为您使用的现有异常类型编写ExceptionMappers.

b)使用"响应"构建器

设置状态代码的另一种方法是使用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();
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

这将返回新创建的用户的JSON表示,但返回状态将为200,而不是201.

现在问题是如果我想使用Response构建器来设置返回代码,我必须Response在我的方法中返回一个对象.我如何仍然返回User要序列化的对象?

a)在servlet响应上设置代码

解决此问题的一种方法是获取servlet请求对象并自己手动设置响应代码,如Garett 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;
}
Run Code Online (Sandbox Code Playgroud)

该方法仍返回实体对象,状态代码为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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

注意:hateoas对象是我开发的帮助类,用于帮助生成资源URI.你需要在这里提出自己的机制;)

就是这样.

我希望这个冗长的回应有助于某人:)

  • 只是我的一个小烦恼:401 并不意味着_用户_未获得授权。这意味着_客户端_未获得授权,因为服务器不知道您是谁。如果不允许已登录/以其他方式识别的用户执行特定操作,则正确的响应代码是 403 Forbidden。 (3认同)

Gar*_*son 69

hedrewness的答案可行,但它修改了让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
}
Run Code Online (Sandbox Code Playgroud)

改进:在找到另一个相关答案之后,我了解到一个人可以注入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
}
Run Code Online (Sandbox Code Playgroud)

  • 我无法覆盖`response.setStatus()`中的状态代码.发送例如**404 Not Found**响应的唯一方法是设置响应状态代码`response.setStatus(404)`en然后关闭输出流`response.getOutputStream().close()`所以JAX -RS无法重置我的状态. (8认同)
  • 我能够使用这种方法设置201代码,但必须添加一个带有`response.flushBuffer()`的try-catch块,以避免框架覆盖我的响应代码.不是很干净. (2认同)

Nth*_*alk 32

如果您希望保持资源层清除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();
}
Run Code Online (Sandbox Code Playgroud)

这是过滤器的主要内容:

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;
        }
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

然后,您的资源上的实现简单地变为:

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 除了@Franter之外,您还可以使用@ Status注释过滤器,而不是在StatusFilter中循环注释.然后,只会在使用@ Status注释的资源上调用过滤器.这是@ NameBinding的目的 (5认同)

erc*_*mar 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();
    }

}
Run Code Online (Sandbox Code Playgroud)


Arr*_*ray 6

我发现用重复的代码构建一个 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();
    }
}
Run Code Online (Sandbox Code Playgroud)


kvi*_*sta 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系统中的应用方式)


Ari*_*iel 5

如果您的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;
}
Run Code Online (Sandbox Code Playgroud)

  • 在我看来,WebApplicationExceptions不适合客户端错误,因为它们会抛出大堆栈跟踪.客户端错误不应抛出服务器端堆栈跟踪并污染日志记录. (3认同)