§ボディパーサー
§ボディパーサーとは?
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-Type
は JsonNode
として解析され、application/x-www-form-urlencoded
の Content-Type
は Map<String, String[]>
として解析されます。
リクエストボディは、Request
の body()
メソッドを介してアクセスでき、RequestBody
オブジェクトでラップされます。これは、ボディになり得るさまざまな型への便利なアクセサーを提供します。たとえば、JSON ボディにアクセスするには
public Result index(Http.Request request) {
JsonNode json = request.body().asJson();
return ok("Got name: " + json.get("name").asText());
}
以下は、デフォルトのボディパーサーでサポートされている型のマッピングです。
text/plain
:String
、asText()
を介してアクセス可能。application/json
:com.fasterxml.jackson.databind.JsonNode
、asJson()
を介してアクセス可能。application/xml
、text/xml
、またはapplication/XXX+xml
:org.w3c.Document
、asXml()
を介してアクセス可能。application/x-www-form-urlencoded
:Map<String, String[]>
、asFormUrlEncoded()
を介してアクセス可能。multipart/form-data
:MultipartFormData
、asMultipartFormData()
を介してアクセス可能。- その他のコンテンツタイプ:
RawBuffer
、asRaw()
を介してアクセス可能。
デフォルトのボディパーサーは、解析を試みる前に、リクエストにボディがあるかどうかを判断しようとします。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
クラスの内部クラスです。簡単に言うと、それらは次のとおりです。
Default
: デフォルトのボディパーサー。AnyContent
: デフォルトのボディパーサーに似ていますが、GET
、HEAD
、DELETE
リクエストのボディを解析します。Json
: ボディを JSON として解析します。TolerantJson
:Json
と似ていますが、Content-Type
ヘッダーが JSON であることを検証しません。Xml
: ボディを XML として解析します。TolerantXml
:Xml
と似ていますが、Content-Type
ヘッダーが XML であることを検証しません。Text
: ボディを文字列として解析します。TolerantText
:Text
と似ていますが、Content-Type
がtext/plain
であることを検証しません。Bytes
: ボディをByteString
として解析します。Raw
: ボディをRawBuffer
として解析します。これは、Play の設定されたメモリバッファーサイズまでメモリにボディを格納しようとしますが、それを超えるとファイルに書き込むようにフォールバックします。FormUrlEncoded
: ボディをフォームとして解析します。MultipartFormData
: ボディをマルチパートフォームとして解析し、ファイル部分をファイルに格納します。Empty
: ボディを解析しません。無視します。
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>>
と同じですが、実際にはこの型のラッパーにすぎません。しかし、大きな違いは、Accumulator
は map
、mapFuture
、recover
などの便利なメソッドを提供し、プロミスのように結果を操作できることです。一方、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
本体に変換できます。変換に失敗した場合は、エラー内容を示すResult
のLeft
を返します。
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);
}
}
§遅延ボディ解析
デフォルトでは、ボディ解析はアクション合成の前に実行されます。ただし、アクション合成の後でボディ解析を遅らせることも可能です。詳細はこちらを参照してください。
次へ: アクション合成
このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送信して貢献してください。ご質問やアドバイスがありましたら、コミュニティフォーラムでコミュニティとの会話を開始してください。