5.Body parsers

(1)什么是Body parsers

一个http请求中,header后面会跟着一个body,header一般是比较小的,可以安全的缓存(buffer)在内存中,因此在中他可以通过RequestHeader类进行构建,然而body可以是可能非常长的,因此他不缓存在内存中但是他也可以作为stream来构建模型。然而,一些请求的body是非常小负荷的并且能在内存中被建模,所以可以把body stream存为一个object内存中,Play提供了一个抽象的BodyParser。
由于Play是一个异步框架,所以传统的InputStream不能用于读取request body——input streams是阻塞,当您调用read时,调用它的线程必须等待数据可用。相反,Play使用的是一个名为Akka流的异步流库。Akka流是一种Reactive Streams的实现,它允许许多异步流api无缝地协同工作,因此传统InputStream的技术不适合与Play一起使用,但Akka Streams和Reactive Streams周边的的整个异步库生态系统将为您提供满足您的一切需求。

(2)深入了解Action

之前我们有介绍过Action 是一个Request=>Result的方法,这是不完全正确的,让我们来更加准确的看一下Action trait

trait Action[A] extends (Request[A] => Result) {
  def parser: BodyParser[A]
}

首先我们看到A是一个泛型,并且Action必须定义一个BodyParse[A]。Request[A]是这样被定义的:

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

A是请求体的类型,我们可以使用任何的scala类型作为请求体,比如String、NodeSeq、JsonValue、Array[Byte]、File等,同样我们可以使用body解析器去解析。

总结一下,Action[A]使用了BodyParser[A]检索到一个来自http请求中的类型A的值,然后构建一个Request[A]对象传递给action代码

(3)使用内置的body parsers

大多数典型的Web应用程序不需要使用自定义的body Parser,它们可以简单地使用Play的内置主体解析器。这些包括用于JSON,XML,表单的解析器,以及将text作为String、作为ByteString。

1.默认的body解析器

如果你没有在http请求头中指定特定的Content-type将会使用默认的body解析器解析对应的body。例如 Content-Type 设置 application/json 将会被当做JsValue进行解析, Content-Type 是application/x-www-form-urlencoded将会被当做Map[String, Seq[String]]进行解析.
默认的body解析器产生类型为AnyContent的body,各种各样的类型都支持从AnyContent通过相应的方法解析为对应的类型,例如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")
  }
}

几个content-type对应解析方法:

  • text/plain: String, accessible via asText.
  • application/json: JsValue, accessible via asJson.
  • application/xml, text/xml or application/XXX+xml: scala.xml.NodeSeq,
    accessible via asXml.
  • application/x-www-form-urlencoded: Map[String, Seq[String]],
  • accessible via asFormUrlEncoded.
  • multipart/form-data: MultipartFormData, accessible via
  • asMultipartFormData.

Any other content type: RawBuffer, accessible via asRaw.
在解析body之前,body解析器会默认的判断是否有body,根据http规范,如果 Content-Length或者Transfer-Encoding存在则标志着body存在,所以当两者任一存在时,解析器将会进行解析。或者当FakeRequest指定了body不为空时会进行解析
如果你像解析任何情况下的

选择一个明确的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都被存入相同的文件中,这是不是有点小问题?让我们再写一个自定义Body解析器,可以从请求的Session中提取用户名:

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

注意:这里不是真正的写一个属于我们自己的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 parser:

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

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

这个函数的签名一开始可能有点吓人,所以让我们把它分解一下。
这个特质传入的是一个RequestHeader对象,用来验证 request的合法性,主要用来获取 Content-Type,所以请求可以正确解析。特质的返回类型是Accumulator,一个accumulator( 聚集器)在 Akka Streams Sink中是轻量级的。一个accumulator会异步地将元素流汇集到result中,这可以通过在 Akka Streams Source中传递来执行。当accumulator结束工作的时候,会返回一个Future对象,这就相当于Sink[E, Future[A]],一个类的封装类,不过有一个大的区别是,Accumulator提供便利的方法,如map, mapFuture, recover等。在这里,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,而你想去stream化它,在另一个地方s,此时需要自定义一个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自定义解析
在极少数情况下会通过Akka Streams来写一个自定义解析器。通常先在ByteString中缓存body是没问题的,另一种更简易的途径在body上是使用必要的方法和随机存取。
当然也有不适合的时候,如果你的body需要解析的内容太长以致于内存中不能匹配合适的空间,这时候你需要写一个自定义解析器。
在来自ByteStrings的流的Parsing Lines下建立起来的CSV Parser,具体使用demo如下,文档来自于Akka Streams cookbook:
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)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值