JAX-RS - 如何同时返回JSON和HTTP状态码?

282

我正在编写一个REST Web应用程序(NetBeans 6.9,JAX-RS,TopLink Essentials),并试图返回JSONHTTP状态代码。我已经准备好了一段代码,并在客户端调用HTTP GET方法时返回JSON。简而言之:

@Path("get/id")
@GET
@Produces("application/json")
public M_機械 getMachineToUpdate(@PathParam("id") String id) {

    // some code to return JSON ...

    return myJson;
}

但我也想在返回JSON数据的同时返回HTTP状态码(500、200、204等)。

我尝试使用HttpServletResponse

response.sendError("error message", 500);

但这会让浏览器认为它是一个“真正”的500,因此输出的网页是一个常规的HTTP 500错误页面。

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


3
500你想要的(不真实?:)和真正的500有什么区别? - razor
@razor 这里的“real 500”意味着HTML错误页面,而不是JSON响应。 - Nupur
1
Web浏览器并非为处理JSON而设计,而是用于HTML页面。因此,如果您响应500(甚至带有一些消息正文),浏览器可能会向普通用户显示错误消息(这实际上取决于浏览器的实现方式),因为这对普通用户很有用。 - razor
14个回答

392

以下是一个示例:

@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")注释该方法。


13
它有效,但我不喜欢响应返回值,因为在我看来它会污染你的代码,特别是对于任何试图使用它的客户端。如果您提供一个返回Response类型的接口给第三方,他不知道您实际返回的是什么类型。Spring通过一个注解更清晰地表达了这一点,如果您始终返回状态码(例如HTTP 204),那么这将非常有用。 - Guido
20
将那个类(Response<T>)改为泛型会是对jax-rs的一个有趣升级,可以同时拥有两种选择的优点。 - Guido
51
无需将实体以某种方式转换为JSON格式。您可以使用return Response.status(Response.Status.Forbidden).entity(myPOJO).build();,它的作用与return myPOJO;相同,但还设置了HTTP状态码。 - kratenko
1
我认为将业务逻辑分离到单独的服务类中效果很好。端点使用响应作为返回类型,其方法大多只是对服务方法的调用以及路径和参数注释。它清晰地将逻辑与URL/内容类型映射分离开来(这就是所谓的实际操作)。 - Stijn de Witt
如果您使用Enunciate,可以使用@TypeHint(Class.class)来为API文档提供特定的类型。 - J. Dimeo
显示剩余3条评论

211

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

让我尝试列举不同的用例及每种情况的解决方案:

1.错误代码(500、404等)

当您想返回与 200 OK 不同的状态代码时,最常见的用例是出现错误。

例如:

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

a)抛出异常

在这种情况下,处理问题的最清洁方法是抛出异常。 这个异常将由 ExceptionMapper 处理,它将将异常转换为具有适当错误代码的响应。

您可以使用默认的Jersey ExceptionMapper (我猜其他实现也是如此),并抛出任何现有的 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构建器来构建预期代码的响应。
在这种情况下,你的方法返回类型必须是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状态代码。

一种方法是像上面描述的那样使用响应对象并自己设置请求正文。 但是,这样做会使您失去使用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请求对象并手动设置响应代码,就像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;
}

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

请注意,为了使其工作,我不得不刷新响应。这是我们漂亮的JAX_RS资源中低级Servlet API代码的不愉快复苏,更糟糕的是,在此之后,它会导致头文件不可修改,因为它们已经被发送到网络中。

在这种情况下,最好的解决方案是使用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();
}

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

结果是HTTP代码是401,正如我们所希望的那样,响应主体与我们之前返回用户对象时完全相同的JSON格式。它还添加了一个位置头。

响应类有许多不同状态(复数形式可能是“stati”)的生成器方法,例如:

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

NB:hateoas对象是我开发的辅助类,用于帮助生成资源URI。您需要自己想出自己的机制;)

大概就是这些。

希望这篇冗长的回答能对某人有所帮助 :)


我想知道是否有一种干净的方法可以返回数据对象本身而不是响应。flush确实很脏。 - AlikElzin-kilaka
2
只是我的一种小抱怨:401 并不意味着“用户”未被授权。它意味着“客户端”未被授权,因为服务器不知道你是谁。如果已登录/已识别的用户不允许执行某个操作,则正确的响应代码是 403 Forbidden。 - Demonblack

72

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
}

改进: 在发现另一个相关的答案后,我了解到即使对于单例服务类(至少在RESTEasy中),也可以将HttpServletResponse作为成员变量注入!这比污染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
}

1
你实际上可以结合这些方法:在方法上使用@Produces进行注释,并且在最终的Response.ok中不指定媒体类型,这样你就可以正确地将返回对象JAXB序列化为与请求匹配的适当媒体类型。(我刚刚尝试了一下,使用一个可以返回XML或JSON的单个方法:除了在@Produces注释中提到之外,该方法本身不需要提及任何内容。) - Royston Shufflebotham
你是对的Garret。我的例子更多地是强调提供内容类型的示例。我们的方法类似,但使用MessageBodyWriterProvider的想法允许隐式内容协商,尽管你的例子似乎缺少一些代码。这是我提供的另一个答案,说明了这一点:https://dev59.com/3W435IYBdhLWcg3w4UMc#5163207 - hisdrewness
8
我无法覆盖 response.setStatus() 中的状态码。例如,发送404 Not Found响应的唯一方法是设置响应状态码response.setStatus(404),然后关闭输出流response.getOutputStream().close(),这样JAX-RS就无法重置我的状态。 - Rob Juurlink
3
我能够使用这种方法设置201代码,但必须添加一个try-catch块,并在其中包含response.flushBuffer(),以避免框架覆盖我的响应代码。这样做不是很干净。 - Pierre Henry
1
@RobJuurlink,如果你想要返回一个特定的404 Not Found错误,最简单的方法可能就是使用throw new NotFoundException() - Garret Wilson

41
如果您想让资源层保持干净,不添加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
很好,谢谢。有了这个和其他一些技巧,我基本上现在能够清理我的代码中的REST API,使其符合一个简单的Java接口,没有提到REST;它只是另一种RMI机制。 - user2800708
6
在StatusFilter中,你可以在@Provider注释之外用@Status注释过滤器,而不是循环注释。然后,该过滤器只会在用@Status注释的资源上调用。这就是@NameBinding的目的。 - trevorism
1
不错的提醒,@trevorism。将StatusFilter注释为@Status有一个不太好的副作用:您要么需要为注释的“值”字段提供默认值,要么在注释类时声明一个值(例如:@Status(200))。这显然不是理想情况。 - Phil

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

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不适用于客户端错误,因为它们会抛出大量的堆栈跟踪信息。客户端错误不应该抛出服务器端的堆栈跟踪信息,并将其混入日志中。 - Rob Juurlink

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

}

5

链接已损坏。 - Umpa

4

我想补充一下,感兴趣的示例是他们定义自己的异常类并在其中构建一个“Response”。只需查找“CustomNotFoundException”类,然后将其复制到您的帖子中即可。 - JBert
我喜欢使用这种方法来处理错误。但是对于成功代码(不同于200的代码),例如“201创建”,则不适用。 - Pierre Henry

2

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

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

它对我来说使用Spring MVC是可以的,但有一种简单的方法可以找出答案! - Caps

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接