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


248

我正在编写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;
}

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

我尝试使用HttpServletResponse

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

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

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


2
您想要的500(不真实的::))和真实的500有什么区别?
剃刀

@razor在这里,实数500表示HTML错误页面,而不是JSON响应
Nupur,

Web浏览器并非设计用于JSON,而是用于HTML页面,因此,如果您使用500(甚至是某些消息正文)进行响应,则浏览器仅会向您显示错误消息(这完全取决于浏览器的实现),因为这对于普通用户。
剃刀

Answers:


346

这是一个例子:

@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),则非常有用
Guido 2012年

19
使该类具有泛型(Response <T>)对于jax-rs而言将是一个有趣的改进,使其具有两种选择的优势。
Guido 2012年

41
无需以某种方式将实体转换为json。您可以return Response.status(Response.Status.Forbidden).entity(myPOJO).build();像使用一样执行Works return myPOJO;,但是需要对HTTP状态代码进行其他设置。
kratenko

1
我认为将业务逻辑分离到一个单独的服务类中效果很好。端点使用Response作为返回类型,它的方法主要只是对服务方法的调用,以及路径和参数注释。它将逻辑与url /内容类型映射(橡胶可以说是很简单)分离开来。
Stijn de Witt

实际上,可以只返回不包装到Response的对象。
ses

191

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

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

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

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

例如:

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

a)抛出异常

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

您可以使用ExceptionMapperJersey预先配置的默认值(我想与其他实现相同)并抛出的任何现有子类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。这在其他各种响应中都有描述,例如历史记录可接受的答案,如下所示:

@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代码的令人不快的死灰复燃,而且更糟的是,这导致报头在此之后无法修改,因为它们已经在网络上发送了。

b)将响应对象与实体一起使用

在这种情况下,最好的解决方案是使用Response对象并设置要在此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吗?)的构建器方法,例如:

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

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

就是这样

我希望这个冗长的回复可以对某人有所帮助:)


我想知道是否有一种干净的方法来返回数据对象本身而不是响应。的flush的确是脏。
AlikElzin-kilaka

1
只是我的宠儿:401并不意味着用户未被授权。这意味着客户端未被授权,因为服务器不知道您是谁。如果不允许已登录/以其他方式识别的用户执行特定操作,则正确的响应代码是403 Forbidden。
Demonblack

69

谨慎的答案将起作用,但是它修改了整个方法,使诸如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,而不必在final中指定媒体类型Response.ok,然后将返回对象正确地JAXB序列化为合适的媒体类型以匹配请求。(我只是用一个可以返回XML或JSON的方法尝试过此方法:方法本身不需要提及,除非在@Produces注释中。)
Royston Shufflebotham 2012年

你是正确的Garret。我的示例更多地说明了提供内容类型的重要性。我们的方法相似,但是使用MessageBodyWriter和的想法Provider允许进行隐式内容协商,尽管您的示例似乎缺少一些代码。下面是我提供了另一种答案,说明这一点:stackoverflow.com/questions/5161466/...
hisdrewness

8
我无法覆盖中的状态代码response.setStatus()。发送例如404 Not Found响应的唯一方法是设置响应状态代码,response.setStatus(404)然后关闭输出流,response.getOutputStream().close()以便JAX-RS无法重置我的状态。
Rob Juurlink 2013年

2
我能够使用这种方法来设置201代码,但是必须添加一个try-catch块,response.flushBuffer()以避免框架覆盖我的响应代码。不太干净。
皮埃尔·亨利

1
@RobJuurlink,如果要专门返回a 404 Not Found,则使用起来可能会更容易throw new NotFoundException()
Garret Wilson

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

@ user2800708,我已经为本地代码执行了此操作,并按照您的建议更新了答案。
Nthalk

很好,谢谢。有了这些和一些技巧,我现在基本上可以清理代码中的REST API,从而使其符合简单的Java接口,而其中没有提及REST。它只是另一个RMI机制。
user2800708'4

6
除了在@@ Provider中使用@Status注释筛选器,而不是在StatusFilter中循环注释。然后,仅在使用@ Status注释的资源上调用过滤器。这是@ NameBinding的目的
trevorism

1
好的标注@trevorism。有注释的一个不那么好的副作用StatusFilter@Status:你要么需要提供对注释的默认value场,或在注解类时声明一个(例如:@Status(200))。这显然是不理想的。
Phil

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不适合客户端错误,因为它们会引发大量堆栈跟踪。客户端错误不应引发服务器端堆栈跟踪并以此污染日志记录。
Rob Juurlink 2013年


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)的Jersey中如何解决。

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

它主要涉及定义自定义Exception并将返回类型保留为实体。发生错误时,将引发异常,否则,将返回POJO。


我想补充一点,感兴趣的示例是他们定义自己的异常类并Response在其中构建一个异常的示例。只需查找CustomNotFoundException课程,然后将其复制到您的帖子中即可。
JBert 2014年

我使用这种方法来处理错误,我喜欢它。但这不适用于成功代码(不同于200),例如“ 201 created”。
皮埃尔·亨利

3

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

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

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

1

另外,请注意,如果HTTP代码为400或更高,默认情况下Jersey将覆盖响应正文。

为了使您指定的实体作为响应主体,请尝试在web.xml配置文件中将以下init-param添加到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>

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

使用Microprofile 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) 

-1

我正在使用带有邮件正文阅读器和编写器的jersey 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();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.