ChatGPT解决这个技术问题 Extra ChatGPT

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 状态代码用于此类事情?

你想要的 500(虚幻?:))和真正的 500 有什么区别?
@razor 这里真正的 500 表示 HTML 错误页面而不是 JSON 响应
Web 浏览器不是为使用 JSON 而设计的,而是使用 HTML 页面,所以如果你用 500 响应(甚至是一些消息正文),浏览器可以只显示一条错误消息(实际上取决于浏览器实现),只是因为这对一个普通用户。

A
Afshin Moazami

这是一个例子:

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


它有效,但我不喜欢 Response 返回值的是,我认为它会污染您的代码,特别是对于任何尝试使用它的客户端。如果你提供一个接口返回一个 Response 给第三方,他不知道你真正返回的是什么类型。 Spring 通过注解使其更加清晰,如果您总是返回状态码(即 HTTP 204),则非常有用
使该类通用 (Response) 将是对 jax-rs 的一个有趣的改进,以同时具有两种替代方案的优点。
无需以某种方式将实体转换为 json。您可以像执行 return myPOJO; 一样执行 return Response.status(Response.Status.Forbidden).entity(myPOJO).build();,但需要额外设置 HTTP 状态代码。
我认为将业务逻辑分离到一个单独的服务类中效果很好。端点使用 Response 作为返回类型,它的方法大多只是对服务方法的调用以及路径和参数注释。它将逻辑与 url / 内容类型映射(可以说是橡胶上路)清晰地分开。
实际上,可以只返回未包装到响应的对象。
A
Amr ElAdawy

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

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

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

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

例如:

请求实体但不存在 (404)

请求在语义上不正确 (400)

用户未被授权 (401)

数据库连接有问题(500)

ETC..

a) 抛出异常

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

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

错误请求异常 (400)

内部服务器错误异常 (500)

未找到异常 (404)

等等。您可以在此处找到列表:API

或者,您可以定义自己的自定义异常和 ExceptionMapper 类,并通过 @Provider 注释 (source of this example) 将这些映射器添加到 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 代码的令人不快的复苏,更糟糕的是,它导致标头在此之后无法修改,因为它们已经在线发送。

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 构建器类的 created 方法将状态代码设置为 201。我们通过 entity() 方法将实体对象(用户)传递给响应。

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

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

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

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

就是这样。

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


我想知道是否有一种干净的方法来返回数据对象本身而不是响应。 flush 确实很脏。
只是我的一个小烦恼:401 并不意味着用户没有被授权。这意味着客户端没有被授权,因为服务器不知道你是谁。如果不允许登录/以其他方式识别的用户执行某个操作,则正确的响应代码是 403 Forbidden。
J
Jasper

hisdrewness 的答案将起作用,但它修改了整个方法,让诸如 Jackson+JAXB 之类的提供者自动将您返回的对象转换为某种输出格式,例如 JSON。受 Apache CXF post(它使用特定于 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
}

改进:在找到另一个相关的 answer 后,我了解到可以将 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 注释中。)
你是对的阁楼。我的示例更多地说明了提供内容类型的重点。我们的方法类似,但使用 MessageBodyWriterProvider 的想法允许隐式内容协商,尽管您的示例似乎缺少一些代码。这是我提供的另一个答案,说明了这一点:stackoverflow.com/questions/5161466/…
我无法覆盖 response.setStatus() 中的状态代码。发送例如 404 Not Found 响应的唯一方法是设置响应状态代码 response.setStatus(404) 然后关闭输出流 response.getOutputStream().close(),以便 JAX-RS 无法重置我的状态。
我可以使用这种方法设置 201 代码,但必须添加一个带有 response.flushBuffer() 的 try-catch 块,以避免框架覆盖我的响应代码。不是很干净。
@RobJuurlink,如果您想专门返回 404 Not Found,使用 throw new NotFoundException() 可能更容易。
N
Nthalk

如果您希望保持资源层中没有 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,我已经为我的本地代码做了这个,按照你的建议更新了答案。
很好,谢谢。有了这个和其他一些技巧,我现在基本上可以清理我的代码中的 REST API,使其符合一个简单的 Java 接口,其中没有提到 REST;它只是另一种 RMI 机制。
除了在@Provider 之外,您还可以使用@Status 对过滤器进行注解,而不是在StatusFilter 中循环注解。然后过滤器只会在带有@Status 注释的资源上调用。这就是@NameBinding的目的
很好的标注@trevorism。使用 @Status 注释 StatusFilter 有一个不太好的副作用:您需要为注释的 value 字段提供默认值,或者在注释类时声明一个默认值(例如:@Status(200))。这显然不理想。
O
Oleg Novosad

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

e
ercalamar

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

}

A
Ariel

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

在我看来,WebApplicationExceptions 不适合客户端错误,因为它们会抛出大量堆栈跟踪。客户端错误不应抛出服务器端堆栈跟踪并用它污染日志记录。
k
kvista

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


链接坏了
a
annouk

请查看此处的示例,它最能说明问题以及在最新 (2.3.1) 版本的 Jersey 中如何解决该问题。

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

它基本上涉及定义自定义异常并将返回类型保持为实体。出错时抛出异常,否则返回 POJO。


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

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

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

使用 Spring MVC 对我来说确实如此,但有一种简单的方法可以找出答案!
M
Maxime T

另外,请注意,默认情况下,如果 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>

J
JBernhardt

以下代码对我有用。通过带注释的 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);
    }
}

t
thatsIch

使用 Microprofile OpenAPI 扩展 Nthalkanswer,您可以使用 @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) 

b
b. phillips

我正在使用带有消息正文阅读器和作者的球衣 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();