1. 序章

このチュートリアルでは、JAX-RS実装であるJerseyを使用して例外を処理するさまざまな方法を見ていきます。

JAX-RSは、例外を処理するための多くのメカニズムを提供し、それらを選択して組み合わせることができます。 REST例外の処理は、より優れたAPIを構築するための重要なステップです。 このユースケースでは、株式を購入するためのAPIを構築し、各ステップが他のステップにどのように影響するかを確認します。

2. シナリオの設定

最小限のセットアップでは、リポジトリ、いくつかのBean、およびいくつかのエンドポイントを作成します。 それは私たちのリソース構成から始まります。 そこで、@ApplicationPathとエンドポイントパッケージを使用して開始URLを定義します。

@ApplicationPath("/exception-handling/*")
public class ExceptionHandlingConfig extends ResourceConfig {
    public ExceptionHandlingConfig() {
        packages("com.baeldung.jersey.exceptionhandling.rest");
    }
}

2.1. 豆

StockWalletの2つのBeanのみが必要なので、Stockを保存して購入できます。 Stock の場合、検証に役立つpriceプロパティが必要です。 さらに重要なことに、 Wallet クラスには、シナリオの構築に役立つ検証メソッドがあります。

public class Wallet {
    private String id;
    private Double balance = 0.0;

    // getters and setters

    public Double addBalance(Double amount) {
        return balance += amount;
    }

    public boolean hasFunds(Double amount) {
        return (balance - amount) >= 0;
    }
}

2.2. エンドポイント

同様に、APIには2つのエンドポイントがあります。 これらは、Beanを保存および取得するための標準的なメソッドを定義します。

@Path("/stocks")
public class StocksResource {
    // POST and GET methods
}
@Path("/wallets")
public class WalletsResource {
    // POST and GET methods
}

たとえば、StocksResourceのGETメソッドを見てみましょう。

@GET
@Path("/{ticker}")
@Produces(MediaType.APPLICATION_JSON)
public Response get(@PathParam("ticker") String id) {
    Optional<Stock> stock = stocksRepository.findById(id);
    stock.orElseThrow(() -> new IllegalArgumentException("ticker"));

    return Response.ok(stock.get())
      .build();
}

GETメソッドでは、最初の例外をスローしています。 後でのみ処理するので、その効果を確認できます。

3. 例外をスローするとどうなりますか?

未処理の例外が発生すると、アプリケーションの内部に関する機密情報が公開される可能性があります。 StocksResourceから存在しないStock でそのGETメソッドを試してみると、次のようなページが表示されます。

このページには、潜在的な攻撃者が脆弱性を悪用するのに役立つ可能性のあるアプリケーションサーバーとバージョンが表示されます。 また、クラス名と行番号に関する情報があり、攻撃者にも役立つ可能性があります。 最も重要なことは、この情報のほとんどはAPIユーザーにとって役に立たず、悪い印象を与えることです。

例外的な応答の制御を支援するために、JAX-RSはクラスExceptionMapperおよびWebApplicationExceptionを提供します。 それらがどのように機能するか見てみましょう。

4. WebApplicationExceptionによるカスタム例外

WebApplicationException を使用すると、カスタム例外を作成できます。 この特別なタイプのRuntimeExceptionを使用すると、応答のステータスとエンティティを定義できます。まず、メッセージとステータスを設定するInvalidTradeExceptionを作成します。

public class InvalidTradeException extends WebApplicationException {
    public InvalidTradeException() {
        super("invalid trade operation", Response.Status.NOT_ACCEPTABLE);
    }
}

また、JAX-RSは、一般的なHTTPステータスコードに対してWebApplicationExceptionのサブクラスを定義しています。 これには、 NotAllowedException BadRequestExceptionなどの便利な例外が含まれます。 ただし、より複雑なエラーメッセージが必要な場合は、JSON応答を返すことができます。

4.1. JSON例外

単純なJavaクラスを作成し、それらをResponseに含めることができます。 この例では、 subject プロパティがあり、これを使用してコンテキストデータをラップします。

public class RestErrorResponse {
    private Object subject;
    private String message;

    // getters and setters
}

この例外は操作されることを意図していないため、subjectのタイプについて心配する必要はありません。

4.2. すべてを使用する

カスタム例外を使用する方法を確認するために、Stockを購入する方法を定義しましょう。

@POST
@Path("/{wallet}/buy/{ticker}")
@Produces(MediaType.APPLICATION_JSON)
public Response postBuyStock(
  @PathParam("wallet") String walletId, @PathParam("ticker") String id) {
    Optional<Stock> stock = stocksRepository.findById(id);
    stock.orElseThrow(InvalidTradeException::new);

    Optional<Wallet> w = walletsRepository.findById(walletId);
    w.orElseThrow(InvalidTradeException::new);

    Wallet wallet = w.get();
    Double price = stock.get()
      .getPrice();

    if (!wallet.hasFunds(price)) {
        RestErrorResponse response = new RestErrorResponse();
        response.setSubject(wallet);
        response.setMessage("insufficient balance");
        throw new WebApplicationException(Response.status(Status.NOT_ACCEPTABLE)
          .entity(response)
          .build());
    }

    wallet.addBalance(-price);
    walletsRepository.save(wallet);

    return Response.ok(wallet)
      .build();
}

この方法では、これまでに作成したすべてのものを使用します。 存在しない株式またはウォレットに対してInvalidTradeExceptionをスローします。また、資金が不足している場合は、Walletを含むRestErrorResponseを作成し、としてスローします。 ]WebApplicationException

4.3. ユースケースの例

まず、ストックを作成しましょう。

$ curl 'http://localhost:8080/jersey/exception-handling/stocks' -H 'Content-Type: application/json' -d '{
    "id": "STOCK",
    "price": 51.57
}'

{"id": "STOCK", "price": 51.57}

次に、Walletで購入します。

$ curl 'http://localhost:8080/jersey/exception-handling/wallets' -H 'Content-Type: application/json' -d '{
    "id": "WALLET",
    "balance": 100.0
}'

{"balance": 100.0, "id": "WALLET"}

その後、ウォレットを使用してストックを購入します。

$ curl -X POST 'http://localhost:8080/jersey/exception-handling/wallets/WALLET/buy/STOCK'

{"balance": 48.43, "id": "WALLET"}

そして、応答で更新された残高を取得します。 また、もう一度購入しようとすると、詳細なRestErrorResponseが表示されます。

{
    "message": "insufficient balance",
    "subject": {
        "balance": 48.43,
        "id": "WALLET"
    }
}

5. ExceptionMapperで未処理の例外

明確にするために、 WebApplicationException をスローするだけでは、デフォルトのエラーページを取り除くのに十分ではありません。 Response のエンティティを指定する必要がありますが、InvalidTradeExceptionの場合はそうではありません。 多くの場合、すべてのシナリオを処理しようとしても、未処理の例外が発生する可能性があります。 したがって、それらを処理することから始めることをお勧めします。 ExceptionMapperを使用して、特定のタイプの例外のキャッチポイントを定義し、コミットする前に応答を変更します。

public class ServerExceptionMapper implements ExceptionMapper<WebApplicationException> {
    @Override
    public Response toResponse(WebApplicationException exception) {
        String message = exception.getMessage();
        Response response = exception.getResponse();
        Status status = response.getStatusInfo().toEnum();

        return Response.status(status)
          .entity(status + ": " + message)
          .type(MediaType.TEXT_PLAIN)
          .build();
    }
}

たとえば、例外情報を Response に再渡すだけで、返される内容が正確に表示されます。 その後、 Response を作成する前に、ステータスコードを確認することで、もう少し先に進むことができます。

switch (status) {
    case METHOD_NOT_ALLOWED:
        message = "HTTP METHOD NOT ALLOWED";
        break;
    case INTERNAL_SERVER_ERROR:
        message = "internal validation - " + exception;
        break;
    default:
        message = "[unhandled response code] " + exception;
}

5.1. 特定の例外の処理

頻繁にスローされる特定のExceptionがある場合は、そのためのExceptionMapperを作成することもできます。 エンドポイントでは、単純な検証のためにIllegalArgumentExceptionをスローするので、そのマッパーから始めましょう。今回は、JSON応答を使用します。

public class IllegalArgumentExceptionMapper
  implements ExceptionMapper<IllegalArgumentException> {
    @Override
    public Response toResponse(IllegalArgumentException exception) {
        return Response.status(Response.Status.EXPECTATION_FAILED)
          .entity(build(exception.getMessage()))
          .type(MediaType.APPLICATION_JSON)
          .build();
    }

    private RestErrorResponse build(String message) {
        RestErrorResponse response = new RestErrorResponse();
        response.setMessage("an illegal argument was provided: " + message);
        return response;
    }
}

これで、未処理の IllegalArgumentException がアプリケーションで発生するたびに、IllegalArgumentExceptionMapperがそれを処理します。

5.2. 構成

例外マッパーをアクティブ化するには、Jerseyリソース構成に戻って、それらを登録する必要があります。

public ExceptionHandlingConfig() {
    // packages ...
    register(IllegalArgumentExceptionMapper.class);
    register(ServerExceptionMapper.class);
}

これは、デフォルトのエラーページを取り除くのに十分です。 次に、スローされる内容に応じて、Jerseyは未処理の例外が発生したときに例外マッパーの1つを使用します。たとえば、存在しない Stock を取得しようとすると、 IllegalArgumentExceptionMapper が使用されます:

$ curl 'http://localhost:8080/jersey/exception-handling/stocks/NONEXISTENT'

{"message": "an illegal argument was provided: ticker"}

同様に、他の未処理の例外については、より広いServerExceptionMapperが使用されます。 たとえば、間違ったHTTPメソッドを使用すると、次のようになります。

$ curl -X POST 'http://localhost:8080/jersey/exception-handling/stocks/STOCK'

Method Not Allowed: HTTP 405 Method Not Allowed

6. 結論

この記事では、Jerseyを使用して例外を処理する多くの方法を見てきました。 さらに、なぜそれが重要なのか、そしてそれをどのように構成するのか。 その後、それらを適用できる簡単なシナリオを作成しました。 その結果、より使いやすく、より安全なAPIが実現しました。

そしていつものように、ソースコードはGitHub利用できます。