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であると考え、出力Webページは通常のHTTP 500エラーページでした。

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


2
欲しい500(unreal?:))と実際の500の違いは何ですか?
かみそり

@razorここで本当の500とは、JSON応答ではなくHTMLエラーページを意味します
Nupur

WebブラウザーはJSONではなくHTMLページで動作するように設計されていないため、500(およびメッセージ本文も)で応答した場合、ブラウザーはエラーメッセージ(実際にはブラウザーの実装に依存します)を表示できます。通常のユーザー。
かみそり

回答:


347

次に例を示します。

@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

19
そのクラスをジェネリック(Response <T>)にすることは、jax-rsの興味深い改善であり、両方の選択肢の利点があります。
グイド

41
エンティティを何らかの方法でjsonに変換する必要はありません。あなたは行うことができますreturn Response.status(Response.Status.Forbidden).entity(myPOJO).build();、あなたが希望の場合のように作品をreturn myPOJO;、しかし、HTTP-ステータスコードの追加設定を持ちます。
クラテンコ2015

1
ビジネスロジックを個別のサービスクラスに分離するとうまくいくと思います。エンドポイントはResponseを戻り値の型として使用し、そのメソッドのほとんどは、サービスメソッドの呼び出しと、パスおよびparamアノテーションです。これにより、ロジックとURL /コンテンツタイプのマッピング(つまり、ゴムがいわば道に突き当たる場所)から明確に分離されます。
Stijn de Witt

実際には、ラッピングしないオブジェクトをResponseに返すだけです。
ses

191

REST WebサービスでHTTPステータスコードを設定するにはいくつかのユースケースがあり、少なくとも1つは既存の回答で十分に文書化されていません(つまり、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アノテーション(この例のソース)を使用してジャージーに追加できます。

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 Builderを使用する

ステータスコードを設定するもう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つの方法は、サーブレットリクエストオブジェクトを取得して、手動で応答コードを設定することです。

@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リソースでの低レベルのサーブレット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クラスには、次のようなさまざまなステータス(stati?)用のいくつかのビルダーメソッドがあります。

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

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

それだけです。

この長い応答が誰かを助けることを願っています:)


応答の代わりにデータオブジェクト自体を返すきれいな方法があるのだろうか。flush確かに汚いです。
AlikElzin-kilaka 2017

1
私のペットピート:401は、ユーザーが承認されいないことを意味しません。これはサーバーがユーザーを認識していないため、クライアントが承認されていないことを意味します。ログに記録された、または他の方法で認識されたユーザーが特定のアクションを実行することが許可されていない場合、正しい応答コードは403 Forbiddenです。
Demonblack

69

hisdrewnessによる回答は機能しますが、Jackson + JAXBなどのプロバイダーが返されたオブジェクトをJSONなどの出力形式に自動的に変換できるようにするアプローチ全体を変更します。Apache CXF 投稿(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
}

改善:別の関連する答えを見つけた後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注釈を除いて、どちらも言及する必要はありません。)
Royston Shufflebotham

あなたは正しいギャレットです。私の例は、コンテンツタイプを提供することに重点が置かれていることを示すものでした。私たちのアプローチは似ていますが、MessageBodyWriterおよびを使用するという考えはProvider、暗黙的なコンテンツネゴシエーションを可能にしますが、例ではコードが不足しているようです。ここで私はこれを示している提供される別の答えがあります:stackoverflow.com/questions/5161466/...
hisdrewness

8
のステータスコードを上書きできませんresponse.setStatus()。たとえば404 Not Found応答を送信する唯一の方法は、応答ステータスコードresponse.setStatus(404)en を設定してから出力ストリームを閉じ、response.getOutputStream().close()JAX-RSがステータスをリセットできないようにすることです。
Rob Juurlink 2013年

2
このアプローチを使用して201コードを設定することができましたがresponse.flushBuffer()、フレームワークが応答コードをオーバーライドするのを回避するために、try-catchブロックを追加する必要がありました。あまりきれいではない。
Pierre Henry 14

1
@RobJuurlink、特にを返す場合は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 2015

@ user2800708、私はすでに私のローカルコードに対してこれを行い、あなたが提案したように答えを更新しました。
Nthalk 2015

よかった、ありがとう。これと他のいくつかのトリックを使用して、基本的にコード内のREST APIをクリーンアップできるようになりました。これにより、RESTについて言及されていない単純なJavaインターフェースに準拠します。これは単なる別のRMIメカニズムです。
user2800708

6
StatusFilterで注釈をループする代わりに、@プロバイダーに加えて@ステータスでフィルターに注釈を付けることができます。次に、フィルタは@ステータスの注釈が付けられたリソースでのみ呼び出されます。これが@ NameBindingの目的です
トレボリズム

1
良い吹き出し@trevorism。で注釈StatusFilterを付けると、それほど良くない副作用が1つあり@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
私の意見では、WebApplicationExceptionは大きなスタックトレースをスローするため、クライアント側のエラーには適していません。クライアントエラーは、サーバー側のスタックトレースをスローして、ログを汚染することはありません。
Rob Juurlink 2013年

5

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フルシステムで想定されている方法です)


リンクが壊れている
Umpa

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

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


興味のある例は、彼らが独自の例外クラスを定義し、その中にを構築するものResponseであることを付け加えたいと思います。CustomNotFoundExceptionクラスを探して、投稿にコピーしてください。
JBert、2014

私はこのアプローチをエラーに使用し、気に入っています。ただし、 '201 created'などの成功コード(200とは異なる)には適用されません。
Pierre Henry 14

3

私はJAX-RSを使用していませんが、使用する同様のシナリオがあります。

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

私はSpring MVCを使用していますが、簡単に見つける方法があります!
キャップ

1

また、httpコードが400以上の場合、デフォルトでJerseyが応答本文をオーバーライドすることに注意してください。

指定したエンティティをレスポンスボディとして取得するには、web.xml構成ファイルのJerseyに次のinit-paramを追加してみます。

    <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

Nthalk答えMicroprofile OpenAPIで拡張すると、@ 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();
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.