ドキュメント

§ボディパーサー

§ボディパーサーとは?

HTTP リクエストはヘッダーとボディで構成されます。ヘッダーは通常小さく、メモリに安全にバッファリングできます。そのため、Play では RequestHeader クラスを使用してモデル化されます。しかし、ボディは非常に長くなる可能性があるため、メモリにバッファリングされず、ストリームとしてモデル化されます。多くのリクエストボディペイロードは小さく、メモリ内でモデル化できるため、ボディストリームをメモリ内のオブジェクトにマッピングするために、Play は BodyParser 抽象化を提供します。

Play は非同期フレームワークであるため、従来の InputStream を使用してリクエストボディを読み取ることができません。入力ストリームはブロッキング型であり、read を呼び出すと、それを呼び出したスレッドはデータが利用可能になるまで待機する必要があります。代わりに、Play は Pekko Streams と呼ばれる非同期ストリーミングライブラリを使用します。Pekko Streams は Reactive Streams の実装であり、多くの非同期ストリーミングAPI をシームレスに連携させる SPI です。そのため、従来の InputStream ベースのテクノロジーは Play に適していませんが、Pekko Streams と Reactive Streams を中心とした非同期ライブラリのエコシステム全体が、必要なものすべてを提供します。

§組み込みボディパーサーの使用

ほとんどの一般的なウェブアプリでは、カスタムボディパーサーを使用する必要はなく、Play の組み込みボディパーサーを使用するだけで済みます。これには、JSON、XML、フォームのパーサーに加えて、プレーンテキストボディを文字列として、バイトボディを ByteString として処理するパーサーが含まれます。

§デフォルトのボディパーサー

ボディパーサーを明示的に選択しない場合に使用されるデフォルトのボディパーサーは、受信した Content-Type ヘッダーを確認し、それに応じてボディを解析します。たとえば、application/json タイプの Content-TypeJsonNode として解析され、application/x-www-form-urlencodedContent-TypeMap<String, String[]> として解析されます。

リクエストボディは、Requestbody() メソッドを介してアクセスでき、RequestBody オブジェクトでラップされます。これは、ボディになり得るさまざまな型への便利なアクセサーを提供します。たとえば、JSON ボディにアクセスするには

public Result index(Http.Request request) {
  JsonNode json = request.body().asJson();
  return ok("Got name: " + json.get("name").asText());
}

以下は、デフォルトのボディパーサーでサポートされている型のマッピングです。

デフォルトのボディパーサーは、解析を試みる前に、リクエストにボディがあるかどうかを判断しようとします。HTTP 仕様によると、Content-Length または Transfer-Encoding ヘッダーのいずれかの存在はボディの存在を示しているため、パーサーはこれらのヘッダーのいずれかが存在する場合、または空ではないボディが明示的に設定されている FakeRequest の場合にのみ解析します。

すべてのケースでボディの解析を試行する場合は、以下で説明されている AnyContent ボディパーサーを使用できます。

§明示的なボディパーサーの選択

ボディパーサーを明示的に選択する場合は、@BodyParser.Of アノテーションを使用して行うことができます。たとえば

@BodyParser.Of(BodyParser.Text.class)
public Result index(Http.Request request) {
  RequestBody body = request.body();
  return ok("Got text: " + body.asText());
}

Play がすぐに提供するボディパーサーはすべて、BodyParser クラスの内部クラスです。簡単に言うと、それらは次のとおりです。

WebSocket に適用されるボディパーサーは無視され、@BodyParser.Of(BodyParser.Empty.class) が使用された場合と同じように機能します。最初の WebSocket リクエストにはボディを含めることができないため、解析は行われません。

§コンテンツ長の制限

組み込みのボディパーサーのほとんどはボディをメモリにバッファリングし、一部はディスクにバッファリングします。バッファリングが制限されていない場合、これはアプリケーションの悪意のある使用または不注意な使用に対する潜在的な脆弱性を引き起こす可能性があります。このため、Play には、メモリバッファリングとディスクバッファリングの 2 つの構成済みバッファー制限があります。

メモリバッファー制限は play.http.parser.maxMemoryBuffer を使用して構成され、デフォルトは 100KB です。ディスクバッファー制限は play.http.parser.maxDiskBuffer を使用して構成され、デフォルトは 10MB です。これらはどちらも application.conf で構成できます。たとえば、メモリバッファー制限を 256KB に増やすには

play.http.parser.maxMemoryBuffer = 256K

カスタムボディパーサーを作成することで、アクションごとに使用されるメモリの量を制限することもできます。以下で詳細を参照してください。

§カスタムボディパーサーの作成

カスタムボディパーサーは、BodyParser クラスを実装することで作成できます。このクラスには、1 つの抽象メソッドがあります。

public abstract Accumulator<ByteString, F.Either<Result, A>> apply(RequestHeader request);

このメソッドのシグネチャは最初は少し分かりにくいので、分解してみましょう。

このメソッドは RequestHeader を受け取ります。これはリクエストに関する情報の確認に使用できます。最も一般的には、Content-Type を取得して、ボディを正しく解析するために使用されます。

このメソッドの戻り値の型は Accumulator です。アキュムレーターは Pekko Streams Sink の薄いレイヤーです。アキュムレーターは、要素のストリームを非同期的に結果に累積します。Pekko Streams Source を渡して実行できます。これは、アキュムレーターが完了したときに償還される CompletionStage を返します。本質的に、Sink<E, CompletionStage<A>> と同じですが、実際にはこの型のラッパーにすぎません。しかし、大きな違いは、AccumulatormapmapFuturerecover などの便利なメソッドを提供し、プロミスのように結果を操作できることです。一方、Sink はそのような操作をすべて mapMaterializedValue 呼び出しでラップする必要があります。

applyメソッドが返すAccumulatorは、ByteString型の要素を消費します。これは本質的にはバイトの配列ですが、byte[]とは異なり、ByteStringは不変であり、スライスや追加などの多くの操作が一定時間で実行されます。

Accumulatorの戻り値の型はF.Either<Result, A>です。これは、Resultを返すか、A型の本体を返すことを意味します。Resultは一般的にエラーの場合に返されます。例えば、本体の解析に失敗した場合、Content-Typeが本体パーサーが受け付ける型と一致しなかった場合、またはメモリバッファの上限を超えた場合などです。本体パーサーがResultを返す場合、アクションの処理はショートサーキットされ、本体パーサーのResultがすぐに返され、アクションは呼び出されません。

§既存のボディパーサーの合成

最初の例として、既存のボディパーサーを合成する方法を示します。定義済みのItemというクラスに受信したJSONを解析したいとします。

まず、JSONボディパーサーに依存する新しいボディパーサーを定義します。

public static class UserBodyParser implements BodyParser<User> {

  private BodyParser.Json jsonParser;
  private Executor executor;

  @Inject
  public UserBodyParser(BodyParser.Json jsonParser, Executor executor) {
    this.jsonParser = jsonParser;
    this.executor = executor;
  }

次に、applyメソッドの実装でJSONボディパーサーを呼び出します。これにより、本体を消費するためのAccumulator<ByteString, F.Either<Result, JsonNode>>が返されます。その後、Promiseのようにマップして、解析されたJsonNode本体をUser本体に変換できます。変換に失敗した場合は、エラー内容を示すResultLeftを返します。

public Accumulator<ByteString, F.Either<Result, User>> apply(RequestHeader request) {
  Accumulator<ByteString, F.Either<Result, JsonNode>> jsonAccumulator =
      jsonParser.apply(request);
  return jsonAccumulator.map(
      resultOrJson -> {
        if (resultOrJson.left.isPresent()) {
          return F.Either.Left(resultOrJson.left.get());
        } else {
          JsonNode json = resultOrJson.right.get();
          try {
            User user = play.libs.Json.fromJson(json, User.class);
            return F.Either.Right(user);
          } catch (Exception e) {
            return F.Either.Left(
                Results.badRequest("Unable to read User from json: " + e.getMessage()));
          }
        }
      },
      executor);
}

返された本体はRequestBodyでラップされ、asメソッドを使用してアクセスできます。

@BodyParser.Of(UserBodyParser.class)
public Result save(Http.Request request) {
  RequestBody body = request.body();
  User user = body.as(User.class);

  return ok("Got: " + user.name);
}

§カスタム最大長ボディパーサーの作成

もう1つのユースケースは、バッファリングのカスタム最大長を使用するボディパーサーを定義することです。Playの組み込みボディパーサーの多くは、このようにバッファ長をオーバーライドできるように拡張するように設計されています。例えば、テキストボディパーサーは次のように拡張できます。

// Accept only 10KB of data.
public static class Text10Kb extends BodyParser.Text {
  @Inject
  public Text10Kb(HttpErrorHandler errorHandler) {
    super(10 * 1024, errorHandler);
  }
}

@BodyParser.Of(Text10Kb.class)
public Result index(Http.Request request) {
  return ok("Got body: " + request.body().asText());
}

§本体の別の場所への転送

これまで、既存のボディパーサーの拡張と合成について説明しました。場合によっては、本体を解析する必要はなく、単に別の場所に転送したい場合があります。例えば、リクエストボディを別のサービスにアップロードしたい場合、カスタムボディパーサーを定義することで実現できます。

public static class ForwardingBodyParser implements BodyParser<WSResponse> {
  private WSClient ws;
  private Executor executor;

  @Inject
  public ForwardingBodyParser(WSClient ws, Executor executor) {
    this.ws = ws;
    this.executor = executor;
  }

  String url = "http://example.com";

  public Accumulator<ByteString, F.Either<Result, WSResponse>> apply(RequestHeader request) {
    Accumulator<ByteString, Source<ByteString, ?>> forwarder = Accumulator.source();

    return forwarder.mapFuture(
        source -> {
          // TODO: when streaming upload has been implemented, pass the source as the body
          return ws.url(url)
              .setMethod("POST")
              // .setBody(source)
              .execute()
              .thenApply(F.Either::Right);
        },
        executor);
  }
}

§Akka Streamsを使用したカスタム解析

まれに、Akka Streamsを使用してカスタムパーサーを作成する必要がある場合があります。ほとんどの場合、上記のようにBytesパーサーを合成することで、本体をByteStringに最初にバッファリングするだけで十分です。これにより、命令型メソッドと本体へのランダムアクセスを使用できるため、通常ははるかに簡単な解析方法が提供されます。

ただし、それが不可能な場合、例えば、解析する必要がある本体がメモリに収まらないほど長い場合、カスタムボディパーサーを作成する必要があります。

Akka Streamsの使用方法の詳細な説明は、このドキュメントの範囲外です。始めるには、Akka Streamsドキュメントを参照することをお勧めします。ただし、以下は、Akka StreamsクックブックのByteStringのストリームからの行の解析ドキュメントに基づいたCSVパーサーを示しています。

public static class CsvBodyParser implements BodyParser<List<List<String>>> {
  private Executor executor;

  @Inject
  public CsvBodyParser(Executor executor) {
    this.executor = executor;
  }

  @Override
  public Accumulator<ByteString, F.Either<Result, List<List<String>>>> apply(
      RequestHeader request) {
    // A flow that splits the stream into CSV lines
    Sink<ByteString, CompletionStage<List<List<String>>>> sink =
        Flow.<ByteString>create()
            // We split by the new line character, allowing a maximum of 1000 characters per line
            .via(Framing.delimiter(ByteString.fromString("\n"), 1000, FramingTruncation.ALLOW))
            // Turn each line to a String and split it by commas
            .map(
                bytes -> {
                  String[] values = bytes.utf8String().trim().split(",");
                  return Arrays.asList(values);
                })
            // Now we fold it into a list
            .toMat(
                Sink.<List<List<String>>, List<String>>fold(
                    new ArrayList<>(),
                    (list, values) -> {
                      list.add(values);
                      return list;
                    }),
                Keep.right());

    // Convert the body to a Right either
    return Accumulator.fromSink(sink).map(F.Either::Right, executor);
  }
}

§遅延ボディ解析

デフォルトでは、ボディ解析はアクション合成の前に実行されます。ただし、アクション合成の後でボディ解析を遅らせることも可能です。詳細はこちらを参照してください。

次へ: アクション合成


このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送信して貢献してください。ご質問やアドバイスがありましたら、コミュニティフォーラムでコミュニティとの会話を開始してください。