§主體解析器
§什麼是主體解析器?
HTTP 要求是標頭後接主體。標頭通常很小,可以在記憶體中安全緩衝,因此在 Play 中使用 RequestHeader
類別建模。然而,主體可能會非常長,因此不會緩衝在記憶體中,而是建模為串流。但是,許多要求主體酬載很小,可以在記憶體中建模,因此為了將主體串流對應到記憶體中的物件,Play 提供 BodyParser
抽象。
由於 Play 是非同步架構,傳統的 InputStream
無法用於讀取要求主體,輸入串流會封鎖,當您呼叫 read
時,呼叫它的執行緒必須等到資料可用。相反地,Play 使用名為 Pekko Streams 的非同步串流函式庫。Pekko Streams 是 Reactive Streams 的實作,一種 SPI,允許許多非同步串流 API 無縫地協同運作,因此儘管傳統基於 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 設定的記憶體緩衝區大小,但如果超過這個大小,就會改寫入File
。FormUrlEncoded
:將主體剖析為表單。MultipartFormData
:將主體剖析為多部分表單,將檔案部分儲存到檔案中。Empty
:不剖析主體,而是忽略它。
套用於 WebSocket 的主體剖析器會被忽略,作用就像使用了 @BodyParser.Of(BodyParser.Empty.class)
。由於最初的 WebSocket 要求不能包含主體,因此不會進行剖析。
§內容長度限制
大多數內建的主體剖析器會將主體緩衝在記憶體中,有些則會將它緩衝在磁碟上。如果緩衝是無限制的,這可能會造成惡意或不小心使用應用程式的潛在漏洞。因此,Play 設定了兩個緩衝限制,一個用於記憶體緩衝,一個用於磁碟緩衝。
記憶體緩衝區限制使用 play.http.parser.maxMemoryBuffer
進行設定,預設為 100KB,而磁碟緩衝區限制使用 play.http.parser.maxDiskBuffer
進行設定,預設為 10MB。這些設定值都可以在 application.conf
中進行設定,例如將記憶體緩衝區限制增加到 256KB
play.http.parser.maxMemoryBuffer = 256K
您也可以透過撰寫自訂主體剖析器來限制每個動作所使用的記憶體量,請參閱 下方 的詳細資訊。
§撰寫自訂主體剖析器
自訂主體剖析器可以透過實作 BodyParser
類別來建立。這個類別有一個抽象方法
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
方法回傳的累加器會使用類型為 ByteString
的元素,它們基本上是位元組陣列,但與 byte[]
不同的是,ByteString
是不可變的,而且許多操作(例如切片和附加)會在恆定時間發生。
累加器的回傳類型為 F.Either<Result, A>
。這表示它將回傳 Result
,或回傳類型為 A
的主體。通常在發生錯誤時回傳結果,例如,如果主體無法解析、Content-Type
與主體剖析器接受的類型不符,或超過記憶體緩衝區。當主體剖析器回傳結果時,這將短路處理動作 - 主體剖析器結果將立即回傳,且動作永遠不會被呼叫。
§組合現有的主體剖析器
作為第一個範例,我們將展示如何組合現有的主體剖析器。假設您想要將一些輸入的 JSON 解析成您定義的名為 Item
的類別。
首先,我們將定義一個新的主體剖析器,它依賴於 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>>
來使用主體。然後,我們可以像承諾一樣對其進行對應,將已解析的 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);
}
§撰寫自訂最大長度主體剖析器
另一個使用案例可能是定義使用自訂最大緩衝長度的主體剖析器。許多內建的 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);
}
}
§使用 Pekko Streams 進行自訂剖析
在罕見的情況下,可能需要使用 Pekko Streams 編寫自訂剖析器。在大部分情況下,先將主體緩衝在 ByteString
中就已足夠,方法是撰寫 Bytes
剖析器,如 上方所述,這通常會提供一種更簡單的剖析方式,因為您可以在主體上使用命令式方法和隨機存取。
但是,當這不可行時,例如當您需要剖析的主體太長而無法放入記憶體中時,您可能需要編寫自訂主體剖析器。
如何使用 Pekko Streams 的完整說明超出了本文件檔的範圍 - 最好的開始方式是閱讀 Pekko Streams 文件檔。但是,以下顯示一個 CSV 剖析器,它建立在 Pekko Streams 食譜中的 從 ByteStrings 串流剖析列 文件檔上
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);
}
}
§延遲主體剖析
預設情況下,主體剖析會在 動作撰寫 發生之前進行。但是,有可能在動作撰寫發生之後延遲主體剖析。可以在 這裡 找到更多詳細資訊。
下一步:動作撰寫
在此文件檔中找到錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件檔指南 之後,請隨時貢獻一個 pull request。有問題或建議要分享?請前往 我們的社群論壇 與社群展開對話。