web-dev-qa-db-ja.com

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

しかしI 同じく はJSONデータと一緒にHTTPステータスコード(500、200、204など)を返したいです。

私はHttpServletResponseを使おうとしました:

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

しかし、これはブラウザにそれが「本物の」500であると考えさせたので、出力されたWebページは通常のHTTP 500エラーページでした。

クライアントサイドのJavaScriptがそれに応じて何らかのロジックを処理できるように、HTTPステータスコードを返したい(たとえば、エラーコードとメッセージをHTMLページに表示するため)。これは可能ですか、それともHTTPステータスコードをそのような目的に使用しないでください。

224
masato-san

これが例です:

@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")というアノテーションを付けることができます。

317
hisdrewness

REST WebサービスでHTTPステータスコードを設定するには、いくつかの使用例があります。少なくとも1つは、既存の回答で十分に文書化されていませんでした。そして、直列化されるオブジェクトだけでなく、デフォルトの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();  
        }
    }

注:使用している既存の例外タイプにExceptionMapperを記述することもできます。

b)Responseビルダーを使用する

ステータスコードを設定するもう1つの方法は、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

戻り状況を設定したい場合のもう1つのケースは、操作が成功したときですが、200ではない成功コードを、本文に返す内容と共に返したい場合があります。

頻繁に使用されるのは、新しいエンティティを作成し(POSTリクエスト)、この新しいエンティティまたはエンティティ自体に関する情報を201 Createdステータスコードと共に返したい場合です。

1つの方法は、上記のようにレスポンスオブジェクトを使用し、リクエストの本文を自分で設定することです。ただし、これを行うと、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表現を返しますが、戻り状況は201ではなく200になります。

Responseビルダーを使用して戻りコードを設定したいのであれば、問題は私のメソッドでResponseオブジェクトを返さなければなりません。シリアル化するUserオブジェクトを返すにはどうすればいいですか?

a)サーブレットレスポンスにコードを設定する

これを解決するための1つの方法は、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になります。

うまく機能させるためには、応答をフラッシュする必要がありました。これは、Nice 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();
}

その場合は、ステータスコードを201に設定するために、Responseビルダークラスの作成済みメソッドを使用します。entity()メソッドを介してエンティティオブジェクト(ユーザー)をレスポンスに渡します。

その結果、HTTPコードは期待どおりの401になり、応答の本体は、Userオブジェクトを返したときの前のJSONとまったく同じになります。ロケーションヘッダーも追加されます。

Responseクラスには、以下のようなさまざまなステータス(静的?)用のいくつかのビルダーメソッドがあります。

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

注:hateoasオブジェクトは、リソースURIを生成するために私が開発したヘルパークラスです。あなたはここであなた自身のメカニズムを考え出す必要があるでしょう;)

それはそれについてです。

この長い反応が誰かに役立つことを願っています:)

175
Pierre Henry

巧妙さによる答えはうまくいくでしょう、しかしそれはJackson + JAXBのようなプロバイダーがあなたの返されたオブジェクトをJSONのような何らかの出力フォーマットに自動的に変換させることへの全体的なアプローチを修正します。 Apache CXFに触発されました post (これはCXF特有のクラスを使用します)JAX-RS実装で動作するはずのレスポンスコードを設定する方法を1つ見つけました: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
}
69
Garret Wilson

リソース層から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;
  }
}
32
Nthalk

例外のためにステータスコードを変更したい場合は、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
ercalamar

あなたの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;
}
5
Ariel

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フルシステムにあるべきである方法です)

5
kvista

このように、繰り返しコードを使って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
Array

私はJAX-RSを使っていませんが、私が使っているのと似たようなシナリオがあります。

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

ここの例を見てください。それは問題とそれがどのように最新の(2.3.1)バージョンのJerseyで解決されるかを最もよく説明します。

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

基本的には、カスタム例外を定義して戻り型をエンティティとして保持する必要があります。エラーがあると、例外がスローされます。それ以外の場合は、POJOを返します。

4
annouk

また、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>
1
Maxime T