一个http请求是一个请求头后面跟着一个请求体,头部信息比较短,可以安全的缓存在内存中,在Play中头部信息使用RequestHeader
类进行建模。请求体的内容可能较大,使用流stream
的形式进行建模。
然而,有许多请求体是比较小的,因此Play提供了一个BodyParser
抽象用于将流中的信息转换为内存对象。由于Play是一个异步框架,对流的处理不使用传统的InputStream
,因为该方法在读取时会阻塞
整个线程直到数据读取完毕。Play中使用异步的Akka Stream进行处理,Akka Stream是Reactive Streams的一个实现。
一、使用play内部的解析器
1.在不指定解析器的情况下,play会使用内部的解析器根据请求头的Content-Type
字段来判断解析后的类型,application/json
会被解析为JsValue,application/x-www-form-urlencoded
会被解析为Map[String, Seq[String]]
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") } }
- text/plain:
String
, accessible viaasText
. - application/json:
JsValue
, accessible viaasJson
. - application/xml, text/xml or application/XXX+xml:
scala.xml.NodeSeq
, accessible viaasXml
. - application/x-www-form-urlencoded:
Map[String, Seq[String]]
, accessible viaasFormUrlEncoded
. - multipart/form-data:
MultipartFormData
, accessible viaasMultipartFormData
. - Any other content type:
RawBuffer
, accessible viaasRaw
.
2.指定解析器类型
def save = Action(parse.json) { request: Request[JsValue] =>Ok("Got: " + (request.body "name").as[String])}
忽略content-type,尽力把请求体解析为json 类型
def save = Action(parse.tolerantJson) { request: Request[JsValue] =>Ok("Got: " + (request.body "name").as[String])}
将请求体存为一个文件
def save = Action(parse.file(to = new File("/tmp/upload"))) { request: Request[File] => Ok("Saved the request content to " + request.body) }
之前是所有用户都存在一个文件里,现在每个用户都存一个文件
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) }
默认内存中最大缓存为100KB,但是可以通过application.conf进行设置: play.http.parser.maxMemoryBuffer=128K
而磁盘最大缓存默认为10MB, play.http.parser.maxDiskBuffer
也可以在函数中设置:
// 设置最大为10KB数据 def save = Action(parse.text(maxLength = 1024 * 10)) { request: Request[String] => Ok("Got: " + text) } def save = Action(parse.maxLength(1024 * 10, storeInUserFile)) { request => Ok("Saved the request content to " + request.body) }
二、自定义一个解析器
1.当你不想解析时,可以把请求体转到别的地方
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") } }
2.通过akka Streams定制解析器
对akka Stream的讨论超出本文的内容,最好去参考akka的相关文档,https://doc.akka.io/docs/akka/2.5/stream/index.html?language=scala,
以下例子是一个CSV文件的解析器,该实例基于
https://doc.akka.io/docs/akka/2.5/stream/stream-cookbook.html?language=scala#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(" "), 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) }