原文: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)
}