§本體剖析器
§什麼是本體剖析器?
HTTP 要求是標頭後接主體。標頭通常很小,可以安全地緩衝在記憶體中,因此在 Play 中,它使用 RequestHeader
類別建模。然而,主體可能很長,因此不會緩衝在記憶體中,而是建模為串流。不過,許多要求主體酬載很小,可以在記憶體中建模,因此 Play 提供 BodyParser
抽象,將主體串流對應到記憶體中的物件。
由於 Play 是非同步架構,傳統的 InputStream
無法用來讀取要求主體,因為輸入串流會封鎖,當您呼叫 read
時,呼叫它的執行緒必須等到有資料可用。相反地,Play 使用名為 Pekko Streams 的非同步串流函式庫。Pekko Streams 是 Reactive Streams 的實作,Reactive Streams 是一種 SPI,讓許多非同步串流 API 能夠無縫地一起運作,因此儘管傳統基於 InputStream
的技術不適合用於 Play,但 Pekko Streams 和 Reactive Streams 周圍的非同步函式庫生態系統將提供您所需的一切。
§更多關於 Action
先前我們說過 Action
是 Request => Result
函式。這並不完全正確。讓我們更精確地檢視 Action
特質
trait Action[A] extends (Request[A] => Result) {
def parser: BodyParser[A]
}
首先,我們看到有一個泛型類型 A
,然後 Action 必須定義 BodyParser[A]
。其中 Request[A]
定義為
trait Request[+A] extends RequestHeader {
def body: A
}
A
類型是要求主體的類型。我們可以使用任何 Scala 類型作為要求主體,例如 String
、NodeSeq
、Array[Byte]
、JsonValue
或 java.io.File
,只要我們有能夠處理它的主體剖析器即可。
總之,Action[A]
使用 BodyParser[A]
從 HTTP 要求中擷取 A
類型的值,並建立傳遞給動作程式碼的 Request[A]
物件。
§使用內建主體剖析器
大多數典型的網路應用程式不需要使用自訂主體剖析器,它們可以只使用 Play 內建的主體剖析器。這些剖析器包括 JSON、XML、表單,以及處理純文字主體作為字串和位元組主體作為 ByteString
。
§預設主體剖析器
如果您沒有明確選擇主體剖析器,則使用的預設主體剖析器會查看傳入的 Content-Type
標頭,並相應地剖析主體。例如,Content-Type
類型的 application/json
將被剖析為 JsValue
,而 Content-Type
類型的 application/x-www-form-urlencoded
將被剖析為 Map[String, Seq[String]]
。
預設主體剖析器產生 AnyContent
類型的主體。AnyContent
支援的各種類型可透過 as
方法存取,例如 asJson
,它會傳回主體類型的 Option
def save: Action[AnyContent] = Action { (request: Request[AnyContent]) =>
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson
// Expecting json body
jsonBody
.map { json => Ok("Got: " + (json \ "name").as[String]) }
.getOrElse {
BadRequest("Expecting application/json request body")
}
}
以下是預設主體剖析器支援的類型對應
- text/plain:
String
,可透過asText
存取。 - application/json:
JsValue
,可透過asJson
存取。 - application/xml、text/xml 或 application/XXX+xml:
scala.xml.NodeSeq
,可透過asXml
存取。 - application/x-www-form-urlencoded:
Map[String, Seq[String]]
,可透過asFormUrlEncoded
存取。 - multipart/form-data:
MultipartFormData
,可透過asMultipartFormData
存取。 - 任何其他內容類型:
RawBuffer
,可透過asRaw
存取。
預設主體剖析器會在嘗試剖析之前,試著判斷要求是否具有主體。根據 HTTP 規格,Content-Length
或 Transfer-Encoding
標頭的存在表示存在主體,因此剖析器僅會在存在其中一個標頭時剖析,或在 FakeRequest
上,當明確設定非空主體時。
如果您想在所有情況下嘗試剖析主體,可以使用 anyContent
主體剖析器,說明如下 下方。
§選擇明確的主體剖析器
如果您想明確選擇主體剖析器,可以透過將主體剖析器傳遞給 Action
apply
或 async
方法來完成。
Play 提供許多現成的主體剖析器,這可透過 PlayBodyParsers
特性取得,可以將其注入控制器中。
因此,例如,要定義一個期待 json 主體的動作(如前一個範例)
def save: Action[JsValue] = Action(parse.json) { (request: Request[JsValue]) =>
Ok("Got: " + (request.body \ "name").as[String])
}
這次請注意,主體的類型是 JsValue
,由於不再是 Option
,因此更容易處理主體。其之所以不是 Option
,是因為 json 主體剖析器會驗證要求的 Content-Type
是否為 application/json
,如果要求不符合此預期,則會傳回 415 不支援的媒體類型
回應。因此,我們不需要在動作程式碼中再次檢查。
這當然表示客戶端必須表現良好,在要求中傳送正確的 Content-Type
標頭。如果您想放寬一點,可以使用 tolerantJson
,它會忽略 Content-Type
並嘗試將主體剖析為 json
def save: Action[JsValue] = Action(parse.tolerantJson) { (request: Request[JsValue]) =>
Ok("Got: " + (request.body \ "name").as[String])
}
以下是另一個範例,它會將要求主體儲存在檔案中
def save: Action[File] = Action(parse.file(to = new File("/tmp/upload"))) { (request: Request[File]) =>
Ok("Saved the request content to " + request.body)
}
§結合主體剖析器
在先前的範例中,所有要求主體都儲存在同一個檔案中。這有點問題,不是嗎?讓我們撰寫另一個自訂主體剖析器,從要求 Session 中萃取使用者名稱,以提供每個使用者一個獨特的文件
val storeInUserFile = parse.using { request =>
request.session
.get("username")
.map { user => parse.file(to = new File("/tmp/" + user + ".upload")) }
.getOrElse {
sys.error("You don't have the right to upload here")
}
}
def save: Action[File] = Action(storeInUserFile) { request => Ok("Saved the request content to " + request.body) }
注意:這裡我們並未真正撰寫我們自己的 BodyParser,而只是結合現有的。這通常就夠了,而且應該涵蓋大多數使用案例。從頭開始撰寫
BodyParser
會在進階主題部分中說明。
§最大內容長度
基於文字的主體剖析器(例如 text、json、xml 或 formUrlEncoded)使用最大內容長度,因為它們必須將所有內容載入記憶體中。預設情況下,它們會剖析的最大內容長度為 100KB。它可以透過在 application.conf
中指定 play.http.parser.maxMemoryBuffer
屬性來覆寫
play.http.parser.maxMemoryBuffer=128K
對於將內容緩衝到磁碟的剖析器,例如原始剖析器或 multipart/form-data
,最大內容長度是使用 play.http.parser.maxDiskBuffer
屬性指定的,預設為 10MB。multipart/form-data
剖析器也會對資料欄位的總和強制執行文字最大長度屬性。
您也可以為特定動作覆寫預設最大長度
// Accept only 10KB of data.
def save: Action[String] = Action(parse.text(maxLength = 1024 * 10)) { (request: Request[String]) =>
Ok("Got: " + text)
}
您也可以用 maxLength
包裝任何主體剖析器
// Accept only 10KB of data.
def save: Action[Either[MaxSizeExceeded, File]] = Action(parse.maxLength(1024 * 10, storeInUserFile)) {
request =>
Ok("Saved the request content to " + request.body)
}
§撰寫自訂主體剖析器
可透過實作 BodyParser
特質來建立自訂主體剖析器。此特質只是一個函式
trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])
此函式的簽章乍看之下可能有點嚇人,因此我們來分解它。
此函式會使用 RequestHeader
。這可用於檢查要求資訊 - 最常見的是用於取得 Content-Type
,以便正確剖析主體。
此函式的回傳類型是 Accumulator
。累加器是 Pekko 串流 Sink
周圍的一層薄薄的包裝。累加器會非同步地將元素串流累加到結果中,它可以透過傳入 Pekko 串流 Source
來執行,這會回傳一個 Future
,當累加器完成時會兌現。它基本上與 Sink[E, Future[A]]
相同,事實上它只是一個包裝在這個類型周圍的包裝器,但最大的不同是 Accumulator
提供了方便的方法,例如 map
、mapFuture
、recover
等,可將結果當作承諾來使用,而 Sink
則需要將所有此類操作包裝在 mapMaterializedValue
呼叫中。
apply
方法回傳的累加器會使用 ByteString
類型的元素 - 它們基本上是位元組陣列,但與 byte[]
的不同之處在於 ByteString
是不可變的,而且許多操作(例如切片和附加)會在固定時間內發生。
累加器的回傳類型是 Either[Result, A]
- 它會回傳 Result
或 A
類型的主體。結果通常會在發生錯誤時回傳,例如,如果主體剖析失敗、Content-Type
與主體剖析器接受的類型不符,或超過記憶體緩衝區時。當主體剖析器回傳結果時,這會短路動作的處理 - 主體剖析器結果會立即回傳,而且動作永遠不會被呼叫。
§將主體導向其他地方
撰寫主體解析器的一個常見使用案例是,當你實際上不想解析主體時,而是想將它串流到其他地方。為執行此操作,你可以定義自訂主體解析器
import javax.inject._
import scala.concurrent.ExecutionContext
import org.apache.pekko.util.ByteString
import play.api.libs.streams._
import play.api.libs.ws._
import play.api.mvc._
class MyController @Inject() (ws: WSClient, val controllerComponents: ControllerComponents)(
implicit ec: ExecutionContext
) extends BaseController {
def forward(request: WSRequest): BodyParser[WSResponse] = BodyParser { req =>
Accumulator.source[ByteString].mapFuture { source =>
request
.withBody(source)
.execute("POST")
.map(Right.apply)
}
}
def myAction: Action[WSResponse] = Action(forward(ws.url("https://example.com"))) { req => Ok("Uploaded") }
}
§使用 Pekko Streams 進行自訂解析
在罕見情況下,可能需要使用 Pekko Streams 撰寫自訂解析器。在大部分情況下,先將主體緩衝在 ByteString
中就已足夠,這通常會提供更簡單的解析方式,因為你可以對主體使用命令式方法和隨機存取。
不過,當這不可行時,例如當你需要解析的主體過長而無法放入記憶體時,你可能需要撰寫自訂主體解析器。
如何使用 Pekko Streams 的完整說明超出本文件說明的範圍 - 最好的入門方式是閱讀 Pekko Streams 文件。不過,以下顯示一個 CSV 解析器,它建立在 從 ByteStrings 串流解析行 的 Pekko Streams 食譜文件說明上
import org.apache.pekko.stream.scaladsl._
import org.apache.pekko.util.ByteString
import play.api.libs.streams._
import play.api.mvc.BodyParser
val Action = inject[DefaultActionBuilder]
val csv: BodyParser[Seq[Seq[String]]] = BodyParser { req =>
// A flow that splits the stream into CSV lines
val sink: Sink[ByteString, Future[Seq[Seq[String]]]] = Flow[ByteString]
// We split by the new line character, allowing a maximum of 1000 characters per line
.via(Framing.delimiter(ByteString("\n"), 1000, allowTruncation = true))
// Turn each line to a String and split it by commas
.map(_.utf8String.trim.split(",").toSeq)
// Now we fold it into a list
.toMat(Sink.fold(Seq.empty[Seq[String]])(_ :+ _))(Keep.right)
// Convert the body to a Right either
Accumulator(sink).map(Right.apply)
}
§延遲主體解析
預設情況下,主體解析會在 動作組成 發生之前進行。不過,有可能在透過動作組成定義的部分 (或全部) 動作處理之後,延遲主體解析。可以在 此處 找到更多詳細資料。
下一步:動作組成
在這份文件中發現錯誤?此頁面的原始程式碼可以在 此處 找到。在閱讀 文件指南 之後,請隨時提出拉取請求。有問題或建議要分享?請前往 我們的社群論壇,與社群展開對話。