【二 HTTP编程】5. Body parsers 原

何为Body parser?

一个HTTP请求由请求头和请求体组成。header部分通常很小 —— 因此可以在内存中被安全的缓存,在Play中对应着RequestHeader模型。相对而言,body部分可能会非常大,这时它不会直接缓存在内存中,而是以流的形式来处理。但是许多请求的请求体也会很小, 可以直接映射到内存,为了将请求体的流看做一个内存对象,Play提供了BodyParser抽象。

Play作为一个异步框架,无法使用传统的InputStream来读取请求体的流——因为InputStream是阻塞的,当你调用read方法时,调用此方法的的线程必须等待数据到达并可用。作为替代,Play提供了一个异步的流处理库——Akka Streams。Akka流是Reactive Stream的实现,一个允许多个异步API无缝集成的SPI。记住基于InputStream的技术在Play中是不适用的,Akka Stream异步库及其完整的生态环境将提供你需要的全部。

关于Actions

前面我们说过Action是一个 Request => Result 函数。这个说法并不完全正确,我们先来看看Action:

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

首先能看到类定义中的泛型A,然后一个action必须定义一个BodyParser[A]。相应的Request[A]定义如下:

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

A类型即请求体的类型。我们可以使用任意Scala类型作为请求体,如: String,NodeSeq,Array[Byte],JsonValue,或者java.io.File,只要有相应的body parser来处理它。

总结一下,Action[A]使用了一个BodyParser[A]来从HTTP请求中获取类型A,然后创建一个Request[A]对象并将它传给action代码。

使用内置的body parsers

大多数的常见的web apps不需要自定义新的body parsers,只需要使用Play内置的body parser就可以工作的很好。包括 JSON,XML,forms及普通文本格式的body体(String)、二进制的body体(ByteString)。

默认的body parser

如果没有显式的指定一个body parser,Play将会根据 Content-Type 选择一个对应的body parser。如,一个Context-Type为application/json将会当做JsValue处理,而application/x-www-from-unlencoded 将会被处理为 Map[String, Seq[String]]

默认的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的映射列表:

  • text/plain:String,对应asText
  • application/json:JsValue,对应 asJson
  • application/xml,text/xml 或 application/XXX+xml:scala.xml.NodeSeq,对应asXml
  • application/x-www-form-urlencoded:Map[String, Seq[String]],对应 asFormUrlEncoded
  • multipart/form-data:MultipartFormData,对应asMultipartFormData。
  • 任意其他类型:RawBuffer,对应 asRaw

默认的body parser在解析前会判断request是否包含了body。HTTP规范规定了 Content-Length / Transfer-Encoding 示意了请求中会带body,因此parser仅在请求提供了这些头时才会解析,还有一种情况就是在 FakeRequest 中明确设置了非空body。

如果你希望在任意情况下都解析body,你可以尝试使用下文中提到的 anyContent body parser。

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

组合body parsers

前面的例子中,所有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(包括 textjsonxml 或者 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)
}

自定义body parser

你可以通过实现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

一个写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来自定义解析

在极少数情况下,你可能需要使用到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)
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章