文档

§主体解析器

§什么是主体解析器?

HTTP 请求由头部和主体组成。头部通常很小 - 可以安全地缓冲在内存中,因此在 Play 中,它使用 RequestHeader 类进行建模。但是,主体可能非常长,因此不会缓冲在内存中,而是被建模为流。但是,许多请求主体有效负载很小,可以在内存中进行建模,因此为了将主体流映射到内存中的对象,Play 提供了 BodyParser 抽象。

由于 Play 是一个异步框架,因此传统的 InputStream 不能用于读取请求主体 - 输入流是阻塞的,当您调用 read 时,调用它的线程必须等待数据可用。相反,Play 使用一个名为 Pekko Streams 的异步流库。Pekko Streams 是 Reactive Streams 的实现,它是一个 SPI,允许许多异步流 API 无缝地协同工作,因此虽然传统的基于 InputStream 的技术不适合与 Play 一起使用,但 Pekko Streams 和围绕 Reactive Streams 的整个异步库生态系统将为您提供所需的一切。

§更多关于 Actions

之前我们说过 Action 是一个 Request => Result 函数。这并不完全正确。让我们更精确地看一下 Action 特性

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

首先,我们看到有一个泛型类型A,然后一个动作必须定义一个BodyParser[A]Request[A]定义为

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

A类型是请求主体类型。我们可以使用任何 Scala 类型作为请求主体,例如StringNodeSeqArray[Byte]JsonValuejava.io.File,只要我们有一个能够处理它的主体解析器。

总之,Action[A]使用BodyParser[A]从 HTTP 请求中检索类型为A的值,并构建一个传递给动作代码的Request[A]对象。

§使用内置主体解析器

大多数典型的 Web 应用程序不需要使用自定义主体解析器,它们可以简单地使用 Play 的内置主体解析器。这些包括 JSON、XML、表单的解析器,以及将纯文本主体处理为字符串和将字节主体处理为ByteString

§默认主体解析器

如果您没有显式选择主体解析器,则使用的默认主体解析器将查看传入的Content-Type标头,并相应地解析主体。例如,类型为application/jsonContent-Type将被解析为JsValue,而Content-Typeapplication/x-www-form-urlencoded将被解析为Map[String, Seq[String]]

默认主体解析器生成类型为AnyContent的主体。AnyContent支持的各种类型可以通过as方法访问,例如asJson,它返回主体类型的Option

def save: Action[AnyContent] = 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")
    }
}

以下是默认主体解析器支持的类型的映射

默认的正文解析器会尝试在解析之前确定请求是否包含正文。根据 HTTP 规范,Content-LengthTransfer-Encoding 标头的存在表示正文的存在,因此解析器只会解析存在这些标头之一的情况,或者在 FakeRequest 中显式设置了非空正文的情况下。

如果您希望在所有情况下尝试解析正文,可以使用 anyContent 正文解析器,如下所述

§选择显式正文解析器

如果您想显式选择正文解析器,可以通过将正文解析器传递给 Actionapplyasync 方法来实现。

Play 提供了许多开箱即用的正文解析器,这些解析器通过 PlayBodyParsers 特性提供,该特性可以注入到您的控制器中。

例如,要定义一个期望 json 正文的动作(如前面的示例)

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

请注意,这次正文的类型是 JsValue,这使得处理正文变得更容易,因为它不再是 Option。它不是 Option 的原因是,json 正文解析器会验证请求是否具有 Content-Typeapplication/json,如果请求不满足该期望,则会返回 415 Unsupported Media Type 响应。因此,我们不需要在操作代码中再次检查。

当然,这意味着客户端必须行为良好,在请求中发送正确的 Content-Type 标头。如果您想稍微放松一点,可以使用 tolerantJson,它会忽略 Content-Type 并尝试将正文解析为 json,无论如何。

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

以下是一个示例,它将请求正文存储在文件中

def save: Action[File] = 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[File] = Action(storeInUserFile) { request => Ok("Saved the request content to " + request.body) }

注意:这里我们并没有真正编写自己的 BodyParser,而只是将现有的 BodyParser 组合在一起。这通常就足够了,并且应该涵盖大多数用例。从头开始编写 BodyParser 将在高级主题部分介绍。

§最大内容长度

基于文本的正文解析器(如 textjsonxmlformUrlEncoded)使用最大内容长度,因为它们必须将所有内容加载到内存中。默认情况下,它们将解析的最大内容长度为 100KB。可以通过在 application.conf 中指定 play.http.parser.maxMemoryBuffer 属性来覆盖它。

play.http.parser.maxMemoryBuffer=128K

对于在磁盘上缓冲内容的解析器,例如原始解析器或 multipart/form-data,最大内容长度使用 play.http.parser.maxDiskBuffer 属性指定,默认值为 10MB。multipart/form-data 解析器还对数据字段的总和强制执行文本最大长度属性。

您也可以覆盖给定操作的默认最大长度。

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

您也可以使用 maxLength 包装任何正文解析器。

// Accept only 10KB of data.
def save: Action[Either[MaxSizeExceeded, File]] = Action(parse.maxLength(1024 * 10, storeInUserFile)) {
  request =>
    Ok("Saved the request content to " + request.body)
}

§编写自定义正文解析器

可以通过实现 BodyParser 特性来创建自定义正文解析器。此特性只是一个函数。

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

此函数的签名乍一看可能有点令人生畏,所以让我们分解一下。

该函数接受一个 RequestHeader。这可以用来检查有关请求的信息 - 最常见的是,它用于获取 Content-Type,以便可以正确解析正文。

该函数的返回类型是一个 Accumulator。累加器是 Pekko Streams Sink 的一个薄层。累加器异步地将元素流累积到一个结果中,它可以通过传入一个 Pekko Streams Source 来运行,这将返回一个 Future,当累加器完成时,该 Future 将被兑现。它本质上与 Sink[E, Future[A]] 相同,事实上它只不过是这种类型的包装器,但最大的区别是 Accumulator 提供了方便的方法,例如 mapmapFuturerecover 等,用于处理结果,就好像它是一个承诺一样,而 Sink 要求所有此类操作都包装在 mapMaterializedValue 调用中。

apply 方法返回的累加器消耗类型为 ByteString 的元素 - 这些本质上是字节数组,但与 byte[] 不同的是 ByteString 是不可变的,并且许多操作(如切片和追加)在恒定时间内发生。

累加器的返回类型为Either[Result, A] - 它将返回一个Result,或者返回一个类型为A的实体。通常在发生错误的情况下返回结果,例如,如果实体解析失败,如果Content-Type与实体解析器接受的类型不匹配,或者如果内存缓冲区已超出。当实体解析器返回结果时,这将短路操作的处理 - 实体解析器的结果将立即返回,并且操作将永远不会被调用。

§将实体重定向到其他地方

编写实体解析器的一个常见用例是,当您实际上不想解析实体时,而是希望将其流式传输到其他地方。为此,您可以定义一个自定义实体解析器

import javax.inject._

import scala.concurrent.ExecutionContext

import org.apache.pekko.util.ByteString
import play.api.libs.streams._
import play.api.libs.ws._
import play.api.mvc._

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("POST")
        .map(Right.apply)
    }
  }

  def myAction: Action[WSResponse] = Action(forward(ws.url("https://example.com"))) { req => Ok("Uploaded") }
}

§使用 Pekko Streams 进行自定义解析

在极少数情况下,可能需要使用 Pekko Streams 编写自定义解析器。在大多数情况下,将实体缓冲在ByteString中就足够了,这通常提供了一种更简单的解析方法,因为您可以使用命令式方法和对实体的随机访问。

但是,当这不可行时,例如,当您需要解析的实体太长而无法放入内存时,您可能需要编写一个自定义实体解析器。

有关如何使用 Pekko Streams 的完整说明超出了本文档的范围 - 最好的起点是阅读Pekko Streams 文档。但是,以下显示了一个 CSV 解析器,它基于从 ByteStrings 流中解析行 Pekko Streams 食谱中的文档

import org.apache.pekko.stream.scaladsl._
import org.apache.pekko.util.ByteString
import play.api.libs.streams._
import play.api.mvc.BodyParser

val Action = inject[DefaultActionBuilder]

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

§延迟实体解析

默认情况下,实体解析发生在操作组合之前。但是,可以将实体解析延迟到通过操作组合定义的一些(或所有)操作处理之后。更多详细信息可以找到这里

下一步:操作组合


发现文档中的错误?此页面的源代码可以在 这里 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区进行交流。