§主体解析器
§什么是主体解析器?
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 类型作为请求主体,例如String
、NodeSeq
、Array[Byte]
、JsonValue
或java.io.File
,只要我们有一个能够处理它的主体解析器。
总之,Action[A]
使用BodyParser[A]
从 HTTP 请求中检索类型为A
的值,并构建一个传递给动作代码的Request[A]
对象。
§使用内置主体解析器
大多数典型的 Web 应用程序不需要使用自定义主体解析器,它们可以简单地使用 Play 的内置主体解析器。这些包括 JSON、XML、表单的解析器,以及将纯文本主体处理为字符串和将字节主体处理为ByteString
。
§默认主体解析器
如果您没有显式选择主体解析器,则使用的默认主体解析器将查看传入的Content-Type
标头,并相应地解析主体。例如,类型为application/json
的Content-Type
将被解析为JsValue
,而Content-Type
为application/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")
}
}
以下是默认主体解析器支持的类型的映射
- 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
访问。
默认的正文解析器会尝试在解析之前确定请求是否包含正文。根据 HTTP 规范,Content-Length
或 Transfer-Encoding
标头的存在表示正文的存在,因此解析器只会解析存在这些标头之一的情况,或者在 FakeRequest
中显式设置了非空正文的情况下。
如果您希望在所有情况下尝试解析正文,可以使用 anyContent
正文解析器,如下所述。
§选择显式正文解析器
如果您想显式选择正文解析器,可以通过将正文解析器传递给 Action
的 apply
或 async
方法来实现。
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-Type
为 application/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
将在高级主题部分介绍。
§最大内容长度
基于文本的正文解析器(如 text、json、xml 或 formUrlEncoded)使用最大内容长度,因为它们必须将所有内容加载到内存中。默认情况下,它们将解析的最大内容长度为 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
提供了方便的方法,例如 map
、mapFuture
、recover
等,用于处理结果,就好像它是一个承诺一样,而 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)
}
§延迟实体解析
默认情况下,实体解析发生在操作组合之前。但是,可以将实体解析延迟到通过操作组合定义的一些(或所有)操作处理之后。更多详细信息可以找到这里。
下一步:操作组合
发现文档中的错误?此页面的源代码可以在 这里 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区进行交流。