一个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) }