什么是 Body 解析器?
HTTP PUT 或 POST 请求含有一个请求体(Body). 请求体可以使用任何格式, 只要在请求头中指定相应的 Content-Type
即可. 在 Play 中, 一个 Body 解析器 转换请求体为对应的 Scala 值.
然而,HTTP 请求体可能非常的大,这时候 Body 解析器 不可能在解析之前光等着把数据全部加载到内存. BodyParser[A]
是个基本的 Iteratee[Array[Byte],A]
, 这就是说它一块一块的接收数据 (只要 Web 浏览器在上传数据) 并计算出类型为 A 的值作为结果.
让我们考虑一下几个例子.
- 一个文本型 Body 解析器能够把逐块的字节数据连缀成一个字符串, 并把计算得到的字符串作为结果 (
Iteratee[Array[Byte],String]
). - 一个文件型 Body 解析器能够把逐块的字节数据存为一个本地文件, 并以
java.io.File
引用作为结果 (Iteratee[Array[Byte],File]
). - 一个 s3 型 Body 解析器能够把逐块的字节数据推送给 Amazon S3 并以 S3 对象 ID 作为结果 (
Iteratee[Array[Byte],S3ObjectId]
).
此外,Body解析器在开始解析数据之前已经访问了 HTTP 请求头, 因而有了机会运行一些先决条件的检查. 例如, Body 解析器能检查一些 HTTP 头是否正确设置了, 或者用户试图上传一个大文件时要检查是否有权限这么做.
Unmi 注:这上面又提了一通 Iteratee,那什么是 Iteratee 呢? 关于 Iteratee IO, 可参考:Iteratee I/O 和 Scalaz Tutorial: Enumeration-Based I/O with Iteratees,Iteratee 是什么意思呢,查不到这个单词,只记得原来看适配器模式时,Adapter 是适配器,Adaptee 是适适配者,同理 Iterator 是迭代器,Iteratee 应该是被迭器者了。
注: 那就是为什么 Body 解析器不是一个真正的
Iteratee[Array[Byte],A]
,确切的说是一个Iteratee[Array[Byte],Either[Result,A]]
, 这就是说它在无法为请求体计算出正确的值时,有能力亲自发出 HTTP 状态码 (像400 BAD_REQUEST
,412 PRECONDITION_FAILED
或者413 REQUEST_ENTITY_TOO_LARGE
)
一旦 Body 解析器完成了它的工作且返回了类型为 A 的值时
, 相应的 Action
函数就会被执行并计算出请求体中的数据值.
更多 Action 的内容
前面我们说 Action
是一个 Request => Result
函数. 这不完全是对的. 让我们更深入地看下 Action
特质:
1 2 3 |
trait Action[A] extends (Request[A] => Result) { def parser: BodyParser[A] } |
首先我们看到有一个类型 A
, 然后这个 Action 必须定义一个 BodyParser[A] 类型
. Request[A]
的定义如下:
1 2 3 |
trait Request[+A] extends RequestHeader { def body: A } |
A
是请求体的类型. 我们可以使用任何 Scala 类型作为请求体的类型, 例如 String
, NodeSeq
, Array[Byte]
, JsonValue
, 或是 java.io.File
, 只要我们有一个 Body 解析器能够处理它.
概括一下就是, Action[A]
用返回类型为 BodyParser[A]
的方法去从 HTTP 请求中获取类型为 A 的值, 并构建出 Request[A]
类型的对象传递给 Action 代码.
默认的 Body 解析器: AnyContent
在我们前面的例子中还从未指定过 Body 解析器. 所以它是怎么工作的呢? 如果你不指定自己的 Body 解析器, Play 就会使用默认的, 它把请求体处理成一个 play.api.mvc.AnyContent 实例
.
Body 解析通过检查 Content-Type
头来决定要处理的 Body 类型:
- text/plain:
String
- application/json:
JsValue
- text/xml:
NodeSeq
- application/form-url-encoded:
Map[String, Seq[String]]
- multipart/form-data:
MultipartFormData[TemporaryFile]
- 任何其他的 content type 类型:
RawBuffer
例如:
1 2 3 4 5 6 7 8 9 10 11 |
def save = Action { request => val body: AnyContent = request.body val textBody: Option[String] = body.asText // Expecting text body textBody.map { text => Ok("Got: " + text) }.getOrElse { BadRequest("Expecting text/plain request body") } } |
Unmi 注: Body 解析器的大致工作过程是,解析请求体中的数据为相应的类型,然后赋值给 request.body,透明化掉 Body 解析器就是你只要正确去使用 request.body 就行,所以你最终要关注的也就是 request.body。request.body 是 AnyContent 类型,它有 asText, asXml, asJson, asRaw, asFormUrlEncoded 和 asMultipartFormData 方法。
指定一个 Body 解析器
Play 中可用的 Body 解析器定义在 play.api.mvc.BodyParsers.parse
中. Unmi 注: 即定义在 ContentTypes.scala 文件中。
所以像本例那样, 定义了一个期望 Text Body 类型的 Action (类似前面示例中那样):
1 2 3 |
def save = Action(parse.text) { request => Ok("Got: " + request.body) } |
Unmi 注: 上面方法的原型是来自于 Action.scala 中的 ActionBuilder 里定义的:
1 2 3 4 |
def apply[A](bodyParser: BodyParser[A])(block: Request[A] => Result): Action[A] = new Action[A] { def parser = bodyParser def apply(ctx: Request[A]) = block(ctx) } |
上面括号里的 parse.text 位置能使用什么还是看看 play.api.mvc.BodyParsers.parse 中定义的方法,见:http://www.playframework.org/documentation/api/2.0/scala/index.html#play.api.mvc.BodyParsers$parse$,其中有
parse.xml, parse.json, parse.temporaryFile, parse.toLerantText, 或者那些带参数的 parse.file (to: File) 等等
前面 parse.text 方法的原型是:
1 2 3 4 |
/** * Parse the body as text if the Content-Type is text/plain. */ def text: BodyParser[String] = text(DEFAULT_MAX_TEXT_LENGTH) |
是不是觉得为什么代码这么简单呢? 这是因为 parse.text
Body 解析在出问题时会发送 400 BAD_REQUEST
响应. 我们不必在自己的 Action 代码中再次检查, 我们可以安全的假定 request.body
中包含的是有效的 String
数据.
Unmi 注: 也就是如果用 parse.text 时,如果发送的请求未指定 Content-Type 为 "text/plain" 时,就会报 400 错,不管实际上发送的是字符串,而用 parse.toLerantText 就不会报错,强行按照 text/plain 来解析。
另外,我们也可以用:
1 2 3 |
def save = Action(parse.tolerantText) { request => Ok("Got: " + request.body) } |
Unmi 注: parse.toLerantText 方法的代码是:
1 2 3 4 |
/** * Parse the body as text without checking the Content-Type. */ def tolerantText: BodyParser[String] = tolerantText(DEFAULT_MAX_TEXT_LENGTH) |
这个方法不会检查 Content-Type
头,且总是把请求体加载为字符串.
小贴士: 在 Play 中所有的 Body 解析都提供有
tolerant
样式的方法.
这是另一个例子, 它将把请求存为一个文件:
1 2 3 |
def save = Action(parse.file(to = new File("/tmp/upload"))) { request => Ok("Saved the request content to " + request.body) } |
组合 Body 解析器
在上一个例子中, 所有的请求体都会存到同一个文件去. 这会产生难以预料的问题,不是吗? 我们来写一个定制的 Body 解析器,它会从请求会话中抽取到用户名, 并以此作为种个用户的唯一文件名:
1 2 3 4 5 6 7 8 9 10 11 |
val storeInUserFile = parse.using { request => request.session.get("username").map { user => file(to = new File("/tmp/" + user + ".upload")) }.getOrElse { error(Unauthorized("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), 但是你也可以内联的指定它:
1 2 3 4 |
// Accept only 10KB of data. def save = Action(parse.text(maxLength = 1024 * 10)) { request => Ok("Got: " + text) } |
小贴士: 默认的内容大小可在
application.conf 中定义
:
parsers.text.maxLength=128K
你还可以在任何的 Body 解析器中用 maxLength 来指定
:
1 2 3 4 |
// Accept only 10KB of data. def save = Action(maxLength(1024 * 10, parser = storeInUserFile)) { request => Ok("Saved the request content to " + request.body) } |
本文链接 https://yanbin.blog/play2-0-tutorials-cn-body-parsers/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。