JAX-RS / Jerseyエラー処理をカスタマイズする方法は?


216

Jerseyを使用してJAX-RS(別名、JSR-311)を学習しています。ルートリソースを正常に作成し、パラメーターをいじっています。

@Path("/hello")
public class HelloWorldResource {

    @GET
    @Produces("text/html")
    public String get(
        @QueryParam("name") String name,
        @QueryParam("birthDate") Date birthDate) {

         // Return a greeting with the name and age
    }
}

これはうまく機能し、Date(String)コンストラクターによって理解される現在のロケールのすべての形式を処理します(YYYY / mm / ddおよびmm / dd / YYYYなど)。しかし、無効な値または理解できない値を指定すると、404応答が返されます。

例えば:

GET /hello?name=Mark&birthDate=X

404 Not Found

この動作をカスタマイズするにはどうすればよいですか?おそらく異なる応答コード(おそらく「400 Bad Request」)でしょうか?エラーの記録についてはどうですか?トラブルシューティングに役立つように、カスタムヘッダーに問題の説明(「不正な日付形式」)を追加することはできますか?または、5xxステータスコードとともに詳細を含むエラー応答全体を返しますか?

回答:


271

JAX-RSを使用してエラー処理動作をカスタマイズする方法はいくつかあります。簡単な3つの方法を次に示します。

最初のアプローチは、WebApplicationExceptionを拡張するExceptionクラスを作成することです。

例:

public class NotAuthorizedException extends WebApplicationException {
     public NotAuthorizedException(String message) {
         super(Response.status(Response.Status.UNAUTHORIZED)
             .entity(message).type(MediaType.TEXT_PLAIN).build());
     }
}

そして、この新しく作成した例外をスローするには、単に次のようにします。

@Path("accounts/{accountId}/")
    public Item getItem(@PathParam("accountId") String accountId) {
       // An unauthorized user tries to enter
       throw new NotAuthorizedException("You Don't Have Permission");
}

WebApplicationExceptionはランタイム例外であるため、throws句で例外を宣言する必要はありません。これにより、クライアントに401応答が返されます。

2番目の簡単な方法はWebApplicationException、コード内で直接のインスタンスを作成する ことです。このアプローチは、独自のアプリケーション例外を実装する必要がない限り機能します。

例:

@Path("accounts/{accountId}/")
public Item getItem(@PathParam("accountId") String accountId) {
   // An unauthorized user tries to enter
   throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}

このコードもクライアントに401を返します。

もちろん、これは単なる例です。必要に応じて例外をさらに複雑にすることができ、必要なHTTP応答コードを生成できます。

他のアプローチの1つは、既存の例外をラップすることです。おそらく、アノテーションが付けられObjectNotFoundExceptionExceptionMapperインターフェースを実装する小さなラッパークラスを使用します@Provider。これは、ラップされた例外が発生した場合に、で定義された応答コードを返すことをJAX-RSランタイムに伝えますExceptionMapper


3
あなたの例では、super()の呼び出しは少し異なるはずです:super(Response.status(Status.UNAUTHORIZED)。entity(message).type( "text / plain")。build()); 洞察をありがとう。
Jon Onstott、2010年

65
質問で述べたシナリオでは、入力値からDateオブジェクトのインスタンスを作成できないため、Jerseyが例外を発生させるため、例外をスローする機会はありません。ジャージー例外をインターセプトする方法はありますか?1つのExceptionMapperインターフェースがありますが、これはメソッドによってスローされた例外もインターセプトします(この場合はget)。
Rejeev Divakaran

7
404が有効なケースでエラーではない場合に例外がサーバーログに表示されるのをどのように回避しますか(つまり、リソースをクエリするたびに、リソースが既に存在するかどうかを確認するために、スタックトレースがサーバーに表示されますログ)。
グイド

3
Jersey 2.xが最も一般的なHTTPエラーコードのいくつかの例外を定義していることに言及する価値があります。したがって、WebApplicationの独自のサブクラスを定義する代わりに、BadRequestExceptionやNotAuthorizedExceptionなどの組み込みのサブクラスを使用できます。たとえば、javax.ws.rs.ClientErrorExceptionのサブクラスを見てください。また、コンストラクタに詳細文字列を指定できることに注意してください。例:throw新しいBadRequestException( "開始日は終了日より前でなければなりません");
バンファー2015

1
あなたはさらに別のアプローチを言及するのを忘れていました:ExceptionMapperインターフェースの実装(それは拡張するより良いアプローチです)。もっとここを参照してくださいvvirlan.wordpress.com/2015/10/19/...
ACV

70
@Provider
public class BadURIExceptionMapper implements ExceptionMapper<NotFoundException> {

public Response toResponse(NotFoundException exception){

    return Response.status(Response.Status.NOT_FOUND).
    entity(new ErrorResponse(exception.getClass().toString(),
                exception.getMessage()) ).
    build();
}
}

上記のクラスを作成します。これは404(NotFoundException)を処理し、toResponseメソッドでカスタム応答を提供できます。同様に、カスタマイズされた応答を提供するためにマップする必要があるParamExceptionなどがあります。


ExceptionMapper <Exception>の実装を一般的な例外にも使用できます
Saurabh

1
これは、JAX-RSクライアントによってスローされたWebApplicationExceptionも処理し、エラーの原因を隠します。カスタム例外(WebApplicationExceptionから派生したものではない)を用意するか、完全な応答でWebApplicationsをスローすることをお勧めします。JAX-RSクライアントによってスローされたWebApplicationExceptionsは、呼び出しで直接処理する必要があります。そうでない場合、別のサービスの応答は、未処理の内部サーバーエラーですが、サービスの応答としてパススルーされます。
Markus Kull

38

Jerseyは、パラメーターの非整列化に失敗した場合にcom.sun.jersey.api.ParamExceptionをスローします。そのため、1つの解決策は、次のタイプの例外を処理するExceptionMapperを作成することです。

@Provider
public class ParamExceptionMapper implements ExceptionMapper<ParamException> {
    @Override
    public Response toResponse(ParamException exception) {
        return Response.status(Status.BAD_REQUEST).entity(exception.getParameterName() + " incorrect type").build();
    }
}

ジャージーがそれを登録するために特にこのマッパーをどこに作成すればよいですか?
Patricio 2013年

1
:あなたがしなければならないのは、詳細についてはこちらを参照してください。@Providerアノテーションを追加することですstackoverflow.com/questions/15185299/...
月Kronquist

27

QueryParam注釈付き変数の再利用可能なクラスを作成することもできます

public class DateParam {
  private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

  private Calendar date;

  public DateParam(String in) throws WebApplicationException {
    try {
      date = Calendar.getInstance();
      date.setTime(format.parse(in));
    }
    catch (ParseException exception) {
      throw new WebApplicationException(400);
    }
  }
  public Calendar getDate() {
    return date;
  }
  public String format() {
    return format.format(value.getTime());
  }
}

次に、次のように使用します。

private @QueryParam("from") DateParam startDateParam;
private @QueryParam("to") DateParam endDateParam;
// ...
startDateParam.getDate();

この場合のエラー処理は簡単です(400応答をスローする)が、このクラスを使用すると、ロギングなどを含む一般的なパラメーター処理を除外できます。


(CXFから移行する)ジャージーにカスタムクエリパラメーターハンドラーを追加しようとしています。これは、私がやっていることと非常によく似ていますが、新しいプロバイダーをインストール/作成する方法がわかりません。あなたの上のクラスは私にこれを示していません。QueryParamにJodaTime DateTimeオブジェクトを使用していますが、それらをデコードするプロバイダーがありません。それをサブクラス化してStringコンストラクターを与え、それを処理するのと同じくらい簡単ですか?
クリスチャンボンジョルノ

1
の代わりにDateParamをラップする上記のようなクラスを作成するだけorg.joda.time.DateTimeですjava.util.Calendar。それ自体で@QueryParamはなく、DateTimeそれを使用します。
チャーリーブルッキング

1
Joda DateTimeを使用している場合、jerseyには直接使用できるDateTimeParamが付属しています。自分で書く必要はありません。github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…を
Srikanth、

これは非常に便利なので追加しますが、ジャクソンをジャージーで使用している場合のみです。Jackson 2.xにはJodaModuleObjectMapper registerModulesメソッドに登録できるがあります。すべてのjoda型変換を処理できます。com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev 2015

11

1つの明白な解決策:文字列を受け取り、自分でDateに変換します。このようにして、必要なフォーマットを定義し、例外をキャッチして、再スローするか、送信されるエラーをカスタマイズできます。解析には、SimpleDateFormatが適切に機能するはずです。

データ型のハンドラーをフックする方法もあると思いますが、この場合、必要なのは単純なコードだけです。


7

私もStaxManがそのQueryParamを文字列として実装し、必要に応じて再スローして変換を処理するのが好きと思います。

ロケール固有の動作が望ましい動作および期待される動作である場合は、以下を使用して400 BAD REQUESTエラーを返します。

throw new WebApplicationException(Response.Status.BAD_REQUEST);

その他のオプションについては、javax.ws.rs.core.Response.StatusのJavaDocを参照してください。


4

@QueryParamドキュメントによると

"注釈付きパラメータ、フィールド、またはプロパティのタイプTは、次のいずれかでなければなりません。

1)プリミティブ型であること
2)単一の文字列引数を受け入れるコンストラクターがあること
3)単一の文字列引数を受け入れることができるvalueOfまたはfromStringという静的メソッドがあること(たとえば、Integer.valueOf(String)を参照)
4)タイプの「文字列から」の変換が可能なjavax.ws.rs.ext.ParamConverterインスタンスを返すjavax.ws.rs.ext.ParamConverterProvider JAX-RS拡張SPIの登録済み実装。
5)Tが上記の2、3、または4を満たすリスト、セット、またはソートセットであること。結果のコレクションは読み取り専用です。」

String形式のクエリパラメータをT型に変換できない場合にユーザーに送信する応答を制御する場合は、WebApplicationExceptionをスローできます。Dropwizardには、必要に応じて使用できる以下の* Paramクラスが付属しています。

BooleanParam、DateTimeParam、IntParam、LongParam、LocalDateParam、NonEmptyStringParam、UUIDParam。https://github.com/dropwizard/dropwizard/tree/master/dropwizard-jersey/src/main/java/io/dropwizard/jersey/paramsを参照してください

Joda DateTimeが必要な場合は、Dropwizard DateTimeParamを使用してください。

上記のリストがニーズに合わない場合は、AbstractParamを拡張して独自のリストを定義してください。解析メソッドをオーバーライドします。エラーレスポンスの本文を制御する必要がある場合は、エラーメソッドをオーバーライドします。

これに関するCoda Haleの良い記事はhttp://codahale.com/what-makes-jersey-interesting-parameter-classes/にあります。

import io.dropwizard.jersey.params.AbstractParam;

import java.util.Date;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class DateParam extends AbstractParam<Date> {

    public DateParam(String input) {
        super(input);
    }

    @Override
    protected Date parse(String input) throws Exception {
        return new Date(input);
    }

    @Override
    protected Response error(String input, Exception e) {
        // customize response body if you like here by specifying entity
        return Response.status(Status.BAD_REQUEST).build();
    }
}

Date(String arg)コンストラクタは非推奨です。Java 8を使用している場合は、Java 8日付クラスを使用します。それ以外の場合は、joda日付時刻をお勧めします。


1

これは実際には正しい動作です。Jerseyは入力のハンドラーを見つけようとし、提供された入力からオブジェクトを構築しようとします。この場合、コンストラクターに提供された値Xで新しいDateオブジェクトを作成しようとします。これは無効な日付であるため、慣例により、ジャージーは404を返します。

あなたができることは、書き換えて生年月日を文字列として入れてから、解析を試み、必要なものが得られない場合は、例外マッピングメカニズムのいずれかによって、必要な例外を自由にスローできます(いくつかあります) )。


1

私は同じ問題に直面していました。

すべてのエラーを中央の場所でキャッチして変換したかったのです。

以下は、それを処理する方法のコードです。

このクラスに注釈を実装ExceptionMapperおよび追加する次のクラスを作成します@Provider。これはすべての例外を処理します。

toResponseメソッドをオーバーライドし、カスタマイズされたデータが入力されたResponseオブジェクトを返します。

//ExceptionMapperProvider.java
/**
 * exception thrown by restful endpoints will be caught and transformed here
 * so that client gets a proper error message
 */
@Provider
public class ExceptionMapperProvider implements ExceptionMapper<Throwable> {
    private final ErrorTransformer errorTransformer = new ErrorTransformer();

    public ExceptionMapperProvider() {

    }

    @Override
    public Response toResponse(Throwable throwable) {
        //transforming the error using the custom logic of ErrorTransformer 
        final ServiceError errorResponse = errorTransformer.getErrorResponse(throwable);
        final ResponseBuilder responseBuilder = Response.status(errorResponse.getStatus());

        if (errorResponse.getBody().isPresent()) {
            responseBuilder.type(MediaType.APPLICATION_JSON_TYPE);
            responseBuilder.entity(errorResponse.getBody().get());
        }

        for (Map.Entry<String, String> header : errorResponse.getHeaders().entrySet()) {
            responseBuilder.header(header.getKey(), header.getValue());
        }

        return responseBuilder.build();
    }
}

// ErrorTransformer.java
/**
 * Error transformation logic
 */
public class ErrorTransformer {
    public ServiceError getErrorResponse(Throwable throwable) {
        ServiceError serviceError = new ServiceError();
        //add you logic here
        serviceError.setStatus(getStatus(throwable));
        serviceError.setBody(getBody(throwable));
        serviceError.setHeaders(getHeaders(throwable));

    }
    private String getStatus(Throwable throwable) {
        //your logic
    }
    private Optional<String> getBody(Throwable throwable) {
        //your logic
    }
    private Map<String, String> getHeaders(Throwable throwable) {
        //your logic
    }
}

//ServiceError.java
/**
 * error data holder
 */
public class ServiceError {
    private int status;
    private Map<String, String> headers;
    private Optional<String> body;
    //setters and getters
}

1

アプローチ1:WebApplicationExceptionクラスを拡張する

WebApplicationExceptionを拡張して新しい例外を作成する

public class RestException extends WebApplicationException {

         private static final long serialVersionUID = 1L;

         public RestException(String message, Status status) {
         super(Response.status(status).entity(message).type(MediaType.TEXT_PLAIN).build());
         }
}

ここで、必要に応じて「RestException」をスローします。

public static Employee getEmployee(int id) {

         Employee emp = employees.get(id);

         if (emp == null) {
                 throw new RestException("Employee with id " + id + " not exist", Status.NOT_FOUND);
         }
         return emp;
}

あなたはこのリンクで完全なアプリケーションを見ることができます。

アプローチ2:ExceptionMapperを実装する

次のマッパーがタイプ 'DataNotFoundException'の例外を処理します

@Provider
public class DataNotFoundExceptionMapper implements
        ExceptionMapper<DataNotFoundException> {

    @Override
    public Response toResponse(DataNotFoundException ex) {
        ErrorMessage model = new ErrorMessage(ex.getErrorCode(),
                ex.getMessage());
        return Response.status(Status.NOT_FOUND).entity(model).build();
    }

}

あなたはこのリンクで完全なアプリケーションを見ることができます。


0

ブラウザのログインウィンドウを開きたい場合の@Steven Lavineの回答の拡張機能と同じです。ユーザーがまだ認証されていない場合、フィルターから応答(MDN HTTP認証)を適切に返すのが難しいことに気付きました

これは、ブラウザのログインを強制する応答を作成するのに役立ちました。ヘッダーの追加の変更に注意してください。これにより、ステータスコードが401に設定され、ブラウザがユーザー名/パスワードダイアログを開くようにするヘッダーが設定されます。

// The extended Exception class
public class NotLoggedInException extends WebApplicationException {
  public NotLoggedInException(String message) {
    super(Response.status(Response.Status.UNAUTHORIZED)
      .entity(message)
      .type(MediaType.TEXT_PLAIN)
      .header("WWW-Authenticate", "Basic realm=SecuredApp").build()); 
  }
}

// Usage in the Filter
if(headers.get("Authorization") == null) { throw new NotLoggedInException("Not logged in"); }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.