一個HTTP請求由請求頭和請求體組成。header部分一般很小 —— 所以能夠在內存中被安全的緩存,在Play中對應着RequestHeader模型。相對而言,body部分可能會很是大,這時它不會直接緩存在內存中,而是以流的形式來處理。可是許多請求的請求體也會很小, 能夠直接映射到內存,爲了將請求體的流看作一個內存對象,Play提供了BodyParser抽象。html
Play做爲一個異步框架,沒法使用傳統的InputStream來讀取請求體的流——由於InputStream是阻塞的,當你調用read方法時,調用此方法的的線程必須等待數據到達並可用。做爲替代,Play提供了一個異步的流處理庫——Akka Streams。Akka流是Reactive Stream的實現,一個容許多個異步API無縫集成的SPI。記住基於InputStream的技術在Play中是不適用的,Akka Stream異步庫及其完整的生態環境將提供你須要的所有。java
前面咱們說過Action是一個 Request => Result 函數。這個說法並不徹底正確,咱們先來看看Action:react
trait Action[A] extends (Request[A] => Result) { def parser: Parser[A] }
首先能看到類定義中的泛型A,而後一個action必須定義一個BodyParser[A]。相應的Request[A]定義以下:web
trait Request[+A] extends RequestHeader { def body: A }
A類型即請求體的類型。咱們可使用任意Scala類型做爲請求體,如: String,NodeSeq,Array[Byte],JsonValue,或者java.io.File,只要有相應的body parser來處理它。json
總結一下,Action[A]使用了一個BodyParser[A]來從HTTP請求中獲取類型A,而後建立一個Request[A]對象並將它傳給action代碼。api
大多數的常見的web apps不須要自定義新的body parsers,只須要使用Play內置的body parser就能夠工做的很好。包括 JSON,XML,forms及普通文本格式的body體(String)、二進制的body體(ByteString)。數組
若是沒有顯式的指定一個body parser,Play將會根據 Content-Type 選擇一個對應的body parser。如,一個Context-Type爲application/json將會當作JsValue處理,而application/x-www-from-unlencoded 將會被處理爲 Map[String, Seq[String]]promise
默認的parser將會建立一個類型爲 AnyContent 的body,AnyContext中的可變可變由 as 方法指定,如asJson將會返回一個Option類型的body:緩存
def save = 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") } }
下面是一張默認body parser的映射列表:安全
默認的body parser在解析前會判斷request是否包含了body。HTTP規範規定了 Content-Length / Transfer-Encoding 示意了請求中會帶body,所以parser僅在請求提供了這些頭時纔會解析,還有一種狀況就是在 FakeRequest 中明確設置了非空body。
若是你但願在任意狀況下都解析body,你能夠嘗試使用下文中提到的 anyContent body parser。
若是你但願顯式指定一個body parser,能夠經過調用Action 的 apply 或 async 方法,向其傳遞一個 body parser。
Play提供了一系列開箱即用的body parser,他們都繼承了PlayBodyParsers特質,能夠直接注入到controller。
一個處理json body的action示例以下:
def save = Action(parse.json) { request: Request[JsValue] => Ok("Got: " + (request.body \ "name").as[String]) }
注意這裏的body類型爲JsValue而非Option,所以變得更加容易處理。內部的機制是json body parser將校驗請求是否有application/json的Content-Type,若是沒有,將直接返回415 Unsupported Media Type。因此咱們的代碼中無需再次檢測。
這意味着提升了對客戶端代碼的規範要求,必須保證他們的Content-Type被正確設置。若是你想放鬆要求,可使用 tolerantJson方法,它會忽略Content-Type,並努力嘗試解析body。
def save = Action(parse.tolerantJson) { request: Request[JsValue] => Ok("Got: " + (request.body \ "name").as[String]) }
下面是一個將request body寫入文件的例子:
def save = Action(parse.file(to = new File("/tmp/upload"))) { request: Request[File] Ok("Save the request content to " + request.body) }
前面的例子中,全部request bodies所有存儲在同一個文件中。下面咱們來從request中解析用戶名,來爲每一個用戶建立單獨的文件:
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(storeInUserFile) { request => Ok("Saved the request content to " + request.body) }
注意:這並非寫一個新的parser,而是組合了已有parser。這種方式已足以應付大多數狀況。關於如何從零開始自定義一個BodyParser將在高級主題中講述。
基於文本的body parsers(包括 text,json,xml 或者 formUrlEncoded)使用了 最大內容長度 (max content length),由於它們須要將整個content載入內存。默認狀況下,最大的content length是100kb。這個值能夠經過設置application.conf中的play.http.parser.maxMemoryBuffer來從新定義:
play.http.parser.maxMemoryBuffer=128K
有些parser會將內容緩存在硬盤上,如 raw parser 或者 multipart/form-data,最大content length由 play.http.parser.maxDiskBuffer定義,默認值是10MB。multipart/form-data parser還會數據字段的聚合強制使用 text max length 屬性。
你能夠在指定action中覆蓋這個設置:
// Accept only 10KB of data. def save = Action(parse.text(maxLength = 1024 * 10)) { request: Request[String] => Ok("Got: " + text) }
你還能夠爲任意的body parser指定maxLength:
// Accept only 10KB of data. def save = Action(parse.maxLength(1024 * 10, storeInUserFile)){ request => Ok("Saved the request content to " + request.body) }
你能夠經過實現BodyParser特質來自定義一個body parser。BodyParser是一個簡單地函數:
trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])
這個函數的簽名看起來有點嚇人,因此下面一塊兒來分解。
函數接受一個RequestHeader。它將被用來檢查request信息 —— 大多數狀況下它將檢查 Content-Type,來保證body以正確的格式被解析。
函數的返回值類型是 Accumulator。累加器(accumulator)是對Akka Streams Sink的簡單封裝。累加器異步的將元素流累積到result中,它能夠經過傳入Akka Streams Source 來運行,並返回一個Future指示累加器的完成狀態。本質上它和Sink[E, Future[A]]是同樣的,事實上也的確如此,它就是在其上的一層封裝。不一樣之處在於Accumulator提供了一系列有用的方法,如map,mapFuture,recover等等,將result視爲一個promise來操做。Sink要求全部這些操做都包含在mapMaterializedValue調用中。
累加器的apply方法返回的是 ByteString 類型 —— 其實就是bytes數組,不一樣之處是ByteString是不可變的,而且以固定時間耗費提供了 slicing、appending 等操做。
累加器的返回值是 Either[Result, A] —— 即返回一個Result,或者一個A類型的body。result通常是在發生錯誤時返回,如者 body parser 不接受此Content-Type類型致使解析失敗,或者超出了內存中的緩存大小限制。當body parser返回一個result時,它將此action短路 —— body parser當即返回,action將不會被調用。
一個寫body parser的常見例子是你不想處理此body,而是想將它引到其它地方。你能夠這樣定義你的parser:
import javax.inject._ import play.api.mvc._ import play.api.libs.streams._ import play.api.libs.ws._ import scala.concurrent.ExecutionContext import akka.util.ByteString 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() .map(Right.apply) } } def myAction = Action(forward(ws.url("https://example.com"))) { req => Ok("Uploaded") } }
在極少數狀況下,你可能須要使用到Akka Streams。大多數狀況下你能夠將body緩存到一個ByteString中,這樣會使操做簡單不少,並且提供了對body的隨機訪問。
可是,當你須要處理很長的body時你就沒法將它整個放入內存。
如何使用Akka Streams已經超出了本文檔的講述範圍。你能夠移步這裏查看 Akka Streams 的細節。咱們下面提供了一個CSV解析器的簡單例子,它基於Akka Streams cookbook 的 Parsing lines from a stream of ByteStrings 部分:
import play.api.mvc.BodyParser import play.api.libs.streams._ import akka.util.ByteString import akka.stream.scaladsl._ 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) }