Scala下Play框架学习笔记(Bodyparsers)-创新互联

什么是Body Parsers

成都创新互联2013年开创至今,先为辉县等服务建站,辉县等地企业,进行企业商务咨询服务。为辉县企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。

     一个HTTP请求是一个头部后面紧随着一个body,头部很小,可以在内存中缓存,因此Play的模型中使用了RequestHeader这个类。Body有时候也可能很长,以致于不能缓存,反而作为一种流而被建模。但是,许多请求体的有效载荷是小的,可以在内存中建模。因此描绘body流对于一个内存中的对象,Play提供 BodyParser

由于Play是一个异步框架,传统的InputStream方法不能用来读请求体,当你调用read方法时,输入流被阻塞,调用它的线程必须等到数据可用为止。Play使用一个异步流的库 Akka 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]}

 BodyParser[A] , 另外Request[A]可以被定义如下:

trait Request[+A] extends RequestHeader {
  def body: A}

  A类型是请求体的类型,我们可以用任何Scala的类型来作为请求体的类型,例如 StringNodeSeqArray[Byte]JsonValue或者java.io.File,只要有一个body parser能够处理它就行。

  总而言之,一个Action[A]会使用一个BodyParser[A]来从HTTP请求中检索A类型的值,来建立传递给Action代码的request[A]类型的对象。

使用内置的body parsers

  许多典型的web apps都不需要使用客户端的body parsers,它们能使用Play内置的body parsers正常工作。包括JSON、XML、表单的解析器,还包括把plain text当做String来处理,把byte当做byteString来处理。

默认的body parser

   当没有明确指定一个body parser的时候,默认的body parser会根据头部的content-type来解析body。举例来说,content-type是Application/json类型的话,会被解析成JSValue,content-type为application/x-www-form-urlencoded类型的会被解析成Map[String, Seq[String]]。

  默认解析器产生的AnyContent类型的body,AnyContent能通过as类方法来支持各种类型,譬如asJson,返回body类型的一个Option类型:

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")
  }}

   默认解析器支持以下类型之间的映射:

   text/plain:通过asText转换成String。

   application/json:通过asJson转换成JSValue。

   application/xml,text/xml或者application/XXX+xml:通过asXML转换成scala.xml.NodeSeq

   application/x-www-form-urlencoded:通过asFormUrlEncoded转换成Map[String, Seq[String]]

   multipart/form-data:通过asMultipartFormData转换成MultipartFormData

   任何其他的类型:通过asRaw转换成rawBuffer。

     默认的body parser,出于性能的考虑,如果请求方法中没有定义一个有意义的body,就不会解析该请求方法的body,默认body parser只解析post、put、patch请求,而不会解析get、head、delete请求,如果要为这些方法解析请求体,就需要使用Anycontent Body Parser。

选择显式的body parser

    如果需要显式地指定body parser,就需要向Action的apply或async方法传递一个body parser。

    Play提供了许多框架之外的body parser,通过用Controllers引入 BodyParsers.parse 对象来实现。举例说明,定义一个期望得到json body的Action如下:

def save = Action(parse.json) { request =>
  Ok("Got: " + (request.body \ "name").as[String])}

    注意到现在body的类型是JSValue,当它不再是Option类型时,工作变得相对简单。没有Option类型的原因是json body parser要验证一个请求有一个application/json的content-type,如果请求没达到期望,然后回送415 Unsupported Media Type应答。因此我们在Action代码中不用再次校验。

    客户端必须发送正确的content-type头部,同时附上他们的请求。如果你想更轻松点,可以使用tolerantJson,这将会忽略content-type,尝试把body解析成json格式:

def save = Action(parse.tolerantJson) { request =>
  Ok("Got: " + (request.body \ "name").as[String])}

    另一个例子是把请求体放在文件里:

def save = Action(parse.file(to = new File("/tmp/upload"))) { request =>
  Ok("Saved the request content to " + request.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)}

    我们不是真正写一个自己的body parser,而是结合已有的body parser而已, 这已经足够了,能涵盖大多数的实例。

大内容长度

    给予文本的body parser,譬如 textjsonxml或者formUrlEncoded这些,使用大内容长度限制,因为他们要将所有内容加载到内存,默认的能解析的大内容长度是100KB,通过指定application.conf中的play.http.parser.maxMemoryBuffer就可以实现:

play.http.parser.maxMemoryBuffer=128K

     对于一个解析器而言,在磁盘上的缓冲内容,譬如raw parser或者multipart/form-data,大内容长度通过play.http.parser.maxDiskBuffer这一属性指定,默认10MB。为了数据域的统计,multipart/form-data解析器强制指定了文本大长度这一属性。

     在Action中也可以修改默认大长度:

// Accept only 10KB of data.def save = Action(parse.text(maxLength = 1024 * 10)) { request =>
  Ok("Got: " + text)}

写一个自定义的body parser:

     通过实现body parser特质,可以实现一个自定义的body parser,body parser特质定义如下:

trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])

     这个特质传入的是一个RequestHeader对象,用来验证请求的合法性,只有得到content-type,请求才能被正确解析。特质的返回类型是Accumulator,一个accumulator在 Akka Streams Sink中是轻量级的。一个accumulator会异步地将元素流汇集到result中,这可以通过在 Akka Streams Source中传递来执行。当accumulator结束工作的时候,会返回一个Future对象,这就相当于Sink[E, Future[A]],一个类的封装类,不过有一个大的区别是,Accumulator提供便利的方法,如mapmapFuturerecover等。处理的是Result类型,因此好像是一个promise,可是Sink实际上所有类似的操作都被封装在mapMaterializedValue回调里。

     Apply方法返回的accumulator产生ByteString类型的元素。这些实际上是Bytes数组,但和byte[]又有所区别, ByteString是不可变的,譬如切分和追加等操作都是在常量时间内完成的。

  如果accumulator的返回类型是Either[Result, A] ,那么它会返回一个Result类型或A类型。A一般是抛出异常时返回的错误类型,这些错误包括解析失败、content-type和body parser接受的类型不匹配,或者缓冲区溢出。如果body parser 返回Result类型,它会缩短Action的过程,body parsers的Result会马上返回,Action永远不会被调用。


定位另一处的body

  一个普通的用例是,当你向解析一个body,并且你希望在另一个地方流式化,此时需要自定义一个body parser:

import javax.inject._import play.api.mvc._import play.api.libs.streams._import play.api.libs.ws._import scala.concurrent.ExecutionContextimport akka.util.ByteStringclass 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来写一个自定义解析器。通常先在ByteString中缓存body是没问题的,另一种更简易的途径在body上是使用必要的方法和随机存取。

   当然也有不适合的时候,如果你的body需要解析的内容太长以致于内存中不能匹配合适的空间,这时候你需要写一个自定义解析器。

   在来自ByteStrings的流的Parsing Lines下建立起来的CSV Parser,具体使用demo如下,文档来自于Akka Streams cookbook:

import play.api.mvc._import play.api.libs.streams._import play.api.libs.concurrent.Execution.Implicits.defaultContextimport akka.util.ByteStringimport 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)}

另外有需要云服务器可以了解下创新互联scvps.cn,海内外云服务器15元起步,三天无理由+7*72小时售后在线,公司持有idc许可证,提供“云服务器、裸金属服务器、高防服务器、香港服务器、美国服务器、虚拟主机、免备案服务器”等云主机租用服务以及企业上云的综合解决方案,具有“安全稳定、简单易用、服务可用性高、性价比高”等特点与优势,专为企业上云打造定制,能够满足用户丰富、多元化的应用场景需求。


文章名称:Scala下Play框架学习笔记(Bodyparsers)-创新互联
文章地址:http://scyanting.com/article/cdgdcp.html