原文:Body parsers
什么是Body解析器?
HTTP請求是一個后面是Body的頭,這個頭通常比較小——它可以安全的緩存在內存中,因此在Play中使用RequestHeader 類模仿頭。而Body有可能很長,因此不能在內存中緩存,而是使用一個流來模仿。
但是,許多請求Body的有效負載比較小,可以緩存到內存中,因此在緩存中映射Body流到一個對象,Play提供了一個 BodyParser 抽象。
由于Paly是一個異步框架,因此傳統的 InputStream不能被用來讀Body請求——當你調用read時,輸入流會被阻塞掉,線程要調用它就必須等到數據可用時。作為替代,Play使用了一個叫做 Akka Streams的異步流庫。
Akka Streams是 Reactive Streams的實現,Reactive Streams是一個允許許多異步流API無縫地一起工作的SPI,因此盡管一般的基于基礎技術的 InputStream不合適在Play中使用,但是Akka Streams和Reactive Streams相關的整個異步庫生態系統將提供給你任何你想要的。
更多關于Actions
前面我們說過,一個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類型是請求Body的類型。只要我們有一個Body解析器能處理這個類型 ,我們就可以使用任何Scala的類型做為請求Body,例如,String, NodeSeq, Array[Byte], JsonValue,或者java.io.File。
總結,Action[A] 使用 BodyParser[A] 從HTTP請求中獲取類型A的值,構建一個可以傳入到Action代碼的Request[A]對象。
使用內置的Body解析器
大多數通常的網絡App不需要自定義Body解析器,他們可以簡單的使用Play的內置Body解析器。這些解析器包括JSON,XML,表單,以及處理純文本字符串和byteBody的ByteString.
如果你沒有明確的選擇Boay解析器它會使用默認的Body解析器,這個默認解析器將在傳入的Content-Type頭中看到,并依此解析Body。所以例如, application/json 類型的 Content-Type將被做為 JsValue解析,而 application/x-www-form-urlencoded類型的Content-Type將被做為 Map[String, Seq[String]]解析。默認的Body解析器產生一個AnyContent類型的Body。 AnyContent支持的各種類型可以通過as方法返回一個Option類型的Body,如asJson。
def save = Action { request =>
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解析器支持的映射類型:
- text/plain: String,通過asText獲得。
- application/json:JsValue,通過asJson.獲得。
- application/xml, text/xml or application/XXX+xml: scala.xml.NodeSeq, 通過asXml獲得。
- application/x-www-form-urlencoded: Map[String, Seq[String]],通過 asFormUrlEncoded得到。
- multipart/form-data: MultipartFormData, 通過asMultipartFormData得到
- 其他的content type RawBuffer, 通過asRaw得到。
由于性能原因,如果請求方法沒有被定義為一個由HTTP規范定義的有意義的Body,那么默認的Body解析器就不會嘗試去解析。也就是說它僅解析POST, PUT 和PATCH請求的Body,而不是GET, HEAD 或者DELETE。如果你想要解析這些方法的請求Body,你可以使用下面將講的anyContentBody解析器。
選擇一個明確的Body解析器
如果你想明確的選擇一個Body解析器,這可以通過傳遞Body解析器到Action 的apply 或者 async 方法。
Play提供了一些可通過BodyParsers.parse對象獲得的現成的Body解析器, 這個對象可以通過Controller特質方便的獲得.因此,例如,要定義一個想要JSON Body的Action(就像前面的例子):
def save = Action(parse.json) { request =>
Ok("Got: " + (request.body \ "name").as[String])
}
注意這次Body的類型是JsValue, 由于它不是Option類型,因此這就讓Body更容易的被處理。它不是 Option 類型的原因是因為請求有application/json的 Content-Type因此Json Body解析器會生效,如果請求沒有匹配到期望的類型,就會返回415 Unsupported Media Type的應答。因此我就不用再次檢查我們的Action代碼。
當然,也就是說,客戶端必須規范,在他們的請求中發送正確的Content-Type頭。如果你想輕松一點,你可以使用忽略Content-Type的tolerantJson,并將其強制解析為Json:
def save = Action(parse.tolerantJson) { request =>
Ok("Got: " + (request.body \ "name").as[String])
}
這里是另一個將請求Body存儲進文件的例子:
def save = Action(parse.file(to = new File("/tmp/upload"))) { request =>
Ok("Saved the request content to " + request.body)
}
合成Body解析器
在前面的例子中:所有的請求Body都被存入相同的文件中,這是不是有點小問題?讓我們再寫一個可以從請求的Session中提取用戶名的自定義Body解析器:
val storeInUserFile = parse.using { request =>
request.session.get("username").map { user =>
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)
}
注意:這里不是真正的寫一個屬于我們自己的BodyParser,而是和已經存在的合成一個。這在大多數情況下是滿足使用的。在高級專題部分講到從零開始寫BodyParser.
最大的內容長度
由于他們必須把所有的內容加載進內存,所以基于文本的Body分析器(如text, json, xml 或 formUrlEncoded)有最大的內容長度限制。默認情況下,被解析的最大的內容長度是 100KB。它可以通過在 pplication.conf文件的
play.http.parser.maxMemoryBuffer=128K
對于那些在磁盤上緩存內容的解析器,如用屬性play.http.parser.maxDiskBuffer指定原生解析器或者 multipart/form-data的最大內容長度,默認是10MB。 multipart/form-data解析器也強迫文本的最大長度屬性為所有數據字段的和.
你也可以使用 maxLength設置任何Body解析器:
// Accept only 10KB of data.
def save = Action(parse.maxLength(1024 * 10, storeInUserFile)) { request =>
Ok("Saved the request content to " + request.body)
}
寫一個自定義的Body解析器
一個自定義的Body解析器可以通過實現 BodyParser 特質完成。這個特質是一個簡單的函數:
trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])
這個函數的簽名開始時可能有點令人望而生畏,因此讓我們來打破這點。函數接受一個 RequestHeader。這可以用來檢查相關最常見的請求信息,也可以用來獲取Content-Type,因此Body可以被正確的解析。函數的返回類型是一個 Accumulator。 Accumulator是 Akka Streams Sink的薄層。Accumulator 異步的把元素流累積到結果中,它可以通過傳入Akka Streams Source運行,當收集器完成時這將返回一個贖回的Future 。它和 Sink[E, Future[A]]本質上是相同的東西,事實上,它只是包裝這個類型的包裝器,但是最大的不同是Accumulator 提供了如map, mapFuture, recover 等等便利的方法。為了使用結果,Sink要求所有的操作被封裝在 mapMaterializedValue里調用。
收集器的 apply 方法返回一個ByteString 類型的consumes(consumes: 指定處理請求的提交內容類型(Content-Type),例如application/json, text/html;)元素——這本質上是Byte數組,但是和 byte[] 的不同之處是 ByteString是不可改變的,并且許多操作如切片和附加在不間斷的時間內執行。收集器的返回類型是 Either[Result, A] ,他要么返回Result,要么返回方法A類型的Body。一般在錯誤的情況下返回A結果,例如,如果Body解析失敗,如果 Content-Type不匹配Body解析器接受的類型,又或者如果超出內存緩沖區。當Body解析器返回一個結果時,這將短路Action的執行——Body解析器結果會被立即返回,并Action將永遠不會被觸發。
把Body跳轉到別處
寫一個Body解析器的常見情況是當你不想解析Body時,相反的,你想把它分流到別處。為了做的這個,你可以定義一個自定義的Body解析器。
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)(implicit ec: ExecutionContext) {
def forward(request: WSRequest): BodyParser[WSResponse] = BodyParser { req =>
Accumulator.source[ByteString].mapFuture { source =>
request
// TODO: stream body when support is implemented
// .withBody(source)
.execute()
.map(Right.apply)
}
}
def myAction = Action(forward(ws.url("https://example.com"))) { req =>
Ok("Uploaded")
}
}
使用Akka Streams自定義解析器
在極少的情況下,需要使用 Akka Streams寫一個自定義的解析器。在大多數情況下,先滿足把Body緩存到 ByteString,由于你對Body使用了命令方法和隨機訪問 , 這通常會提供一種簡單的解析方式。然而,當那不可行時,例如,當你需要解析的Body太長而不能放入緩存時,那么你需要寫一個自定義的Body解析器。
怎么使用 Akka Streams 的完整描述超出了本文檔的范圍——最好是從閱讀 Akka Streams 文檔開始。不管怎樣,下面介紹CSV解析器,它基于 Akka Streams cookbook中的文檔 Parsing lines from a stream of ByteStrings
import play.api.mvc._
import play.api.libs.streams._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
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)
}