簡體   English   中英

JAX-RS — 如何同時返回 JSON 和 HTTP 狀態代碼?

[英]JAX-RS — How to return JSON and HTTP status code together?

我正在編寫 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 狀態代碼用於此類事情?

下面是一個例子:

@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")注釋該方法

在 REST Web 服務中設置 HTTP 狀態代碼有多種用例,至少有一個沒有在現有答案中充分記錄(即,當您使用 JAXB 使用自動神奇的 JSON/XML 序列化,並且您想返回一個要序列化的對象,但也是一個不同於默認 200 的狀態代碼)。

因此,讓我嘗試列舉不同的用例和每個用例的解決方案:

1. 錯誤代碼 (500, 404,...)

當您想要返回不同於200 OK的狀態代碼時,最常見的用例是發生錯誤時。

例如:

  • 請求一個實體,但它不存在 (404)
  • 請求在語義上不正確 (400)
  • 用戶未被授權 (401)
  • 數據庫連接有問題(500)
  • 等..

a) 拋出異常

在那種情況下,我認為處理問題的最干凈的方法是拋出異常。 此異常將由ExceptionMapper處理,它將異常轉換為具有適當錯誤代碼的響應。

您可以使用 Jersey 預先配置的默認ExceptionMapper (我猜它與其他實現相同)並拋出javax.ws.rs.WebApplicationException任何現有子類。 這些是預定義的異常類型,它們預先映射到不同的錯誤代碼,例如:

  • 錯誤請求異常 (400)
  • 內部服務器錯誤異常 (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 對象通用以指示有效負載實體的類型會很好,但目前情況並非如此。

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

注意: hatoas 對象是我開發的一個幫助類,用於幫助生成資源 URI。 您需要在這里提出自己的機制;)

就是這樣。

我希望這個冗長的回復對某人有所幫助:)

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
}

改進:在找到另一個相關答案后,我了解到可以將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
}

如果你想讓你的資源層沒有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;
  }
}

如果您因為異常而想要更改狀態代碼,使用 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;
}

我發現用重復的代碼構建一個 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();
    }
}

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 系統中應該是這樣)

請看這里的例子,它最好地說明了這個問題以及它在最新的(2.3.1)版本的 Jersey 中是如何解決的。

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

它基本上涉及定義自定義異常並將返回類型保留為實體。 有錯誤時拋出異常,否則返回POJO。

我沒有使用 JAX-RS,但我有一個類似的場景,我使用:

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

另外,請注意,默認情況下,如果 http 代碼為 400 或更多,Jersey 將覆蓋響應正文。

為了讓您指定的實體作為響應正文,請嘗試將以下 init-param 添加到您的 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);
    }
}

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

我正在將 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();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM