文档章节

Body parsers【翻译】

我是菜鸟我骄傲
 我是菜鸟我骄傲
发布于 2017/04/22 10:50
字数 2419
阅读 12
收藏 0

原文:Body parsers #什么是Body解析器?

HTTP请求是一个后面是Body的头,这个头通常比较小——它可以安全的缓存在内存中,因此在Play中使用RequestHeader 类模仿头。而Body有可能很长,因此不能在内存中缓存,而是使用一个流来模仿。

但是,许多请求Body的有效负载比较小,可以缓存到内存中,因此在缓存中映射Body流到一个对象,Play提供了一个 BodyParser 抽象。

由于Paly是一个异步框架,因此传统的 InputStream不能被用来读Body请求——当你调用read时,输入流会被阻塞掉,线程要调用它就必须等到数据可用时。作为替代,Play使用了一个叫做 Akka Streams的异步流库。

Akka Streams是 Reactive Streams的实现,Reactive Streams是一个允许许多异步流API无缝地一起工作的SPI,因此尽管一般的基于基础技术的 InputStream不合适在Play中使用,但是Akka Streams和Reactive Streams相关的整个异步库生态系统将提供给你任何你想要的。

更多关于Actions

前面我们说过,一个Action是一个Request => Result函数。这不完全是对的。让我们更精确的看一看Action的特质:

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

首先我们看到有一个泛型类型A,然后Action必须定义一个BodyParser[A].。同时Request[A]被定义为:

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

A类型是请求Body的类型。只要我们有一个Body解析器能处理这个类型 ,我们就可以使用任何Scala的类型做为请求Body,例如,String, NodeSeq, Array[Byte], JsonValue,或者java.io.File。

总结,Action[A] 使用 BodyParser[A] 从HTTP请求中获取类型A的值,构建一个可以传入到Action代码的Request[A]对象。

使用内置的Body解析器

大多数通常的网络App不需要自定义Body解析器,他们可以简单的使用Play的内置Body解析器。这些解析器包括JSON,XML,表单,以及处理纯文本字符串和byteBody的ByteString.

如果你没有明确的选择Boay解析器它会使用默认的Body解析器,这个默认解析器将在传入的Content-Type头中看到,并依此解析Body。所以例如, application/json 类型的 Content-Type将被做为 JsValue解析,而 application/x-www-form-urlencoded类型的Content-Type将被做为 Map[String, Seq[String]]解析。默认的Body解析器产生一个AnyContent类型的Body。 AnyContent支持的各种类型可以通过as方法返回一个Option类型的Body,如asJson。

def save = Action { request =>
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解析器支持的映射类型:

  • text/plain: String,通过asText获得。
  • application/json:JsValue,通过asJson.获得。
  • application/xml, text/xml or application/XXX+xml: scala.xml.NodeSeq, 通过asXml获得。
  • application/x-www-form-urlencoded: Map[String, Seq[String]],通过 asFormUrlEncoded得到。
  • multipart/form-data: MultipartFormData, 通过asMultipartFormData得到
  • 其他的content type RawBuffer, 通过asRaw得到。

由于性能原因,如果请求方法没有被定义为一个由HTTP规范定义的有意义的Body,那么默认的Body解析器就不会尝试去解析。也就是说它仅解析POST, PUT 和PATCH请求的Body,而不是GET, HEAD 或者DELETE。如果你想要解析这些方法的请求Body,你可以使用下面将讲的anyContentBody解析器。

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

val storeInUserFile = parse.using { request =>
request.session.get("username").map { user =>
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解析器

一个自定义的Body解析器可以通过实现 BodyParser 特质完成。这个特质是一个简单的函数:

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

这个函数的签名开始时可能有点令人望而生畏,因此让我们来打破这点。函数接受一个 RequestHeader。这可以用来检查相关最常见的请求信息,也可以用来获取Content-Type,因此Body可以被正确的解析。函数的返回类型是一个 Accumulator。 Accumulator是 Akka Streams Sink的薄层。Accumulator 异步的把元素流累积到结果中,它可以通过传入Akka Streams Source运行,当收集器完成时这将返回一个赎回的Future 。它和 Sink[E, Future[A]]本质上是相同的东西,事实上,它只是包装这个类型的包装器,但是最大的不同是Accumulator 提供了如map, mapFuture, recover 等等便利的方法。为了使用结果,Sink要求所有的操作被封装在 mapMaterializedValue里调用。

收集器的 apply 方法返回一个ByteString 类型的consumes(consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;)元素——这本质上是Byte数组,但是和 byte[] 的不同之处是 ByteString是不可改变的,并且许多操作如切片和附加在不间断的时间内执行。收集器的返回类型是 Either[Result, A] ,他要么返回Result,要么返回方法A类型的Body。一般在错误的情况下返回A结果,例如,如果Body解析失败,如果 Content-Type不匹配Body解析器接受的类型,又或者如果超出内存缓冲区。当Body解析器返回一个结果时,这将短路Action的执行——Body解析器结果会被立即返回,并Action将永远不会被触发。

把Body跳转到别处

写一个Body解析器的常见情况是当你不想解析Body时,相反的,你想把它分流到别处。为了做的这个,你可以定义一个自定义的Body解析器。

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)(implicit ec: ExecutionContext) {

def forward(request: WSRequest): BodyParser[WSResponse] = BodyParser { req =>
Accumulator.source[ByteString].mapFuture { source =>
request
// TODO: stream body when support is implemented
// .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太长而不能放入缓存时,那么你需要写一个自定义的Body解析器。

怎么使用 Akka Streams 的完整描述超出了本文档的范围——最好是从阅读 Akka Streams 文档开始。不管怎样,下面介绍CSV解析器,它基于 Akka Streams cookbook中的文档 Parsing lines from a stream of ByteStrings

import play.api.mvc._
import play.api.libs.streams._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
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)
}

© 著作权归作者所有

共有 人打赏支持
我是菜鸟我骄傲
粉丝 13
博文 238
码字总数 150711
作品 0
西安
架构师
私信 提问
play2.0文档-面向java开发者(5)

Body parsers Body解析器 What is a body parser? body解析器是啥? An HTTP request (at least for those using the POST and PUT operations) contains a body. This body can be formatt......

老盖
2012/04/10
0
0
play系列6: request body解析

import play.mvc.Http.RequestBody; 获得方式:RequestBody body = request().body(); #1 由于request body有各种形式,所以使用前需要指定某一个特定格式的解析器对request body 进行解析以...

强子哥哥
2014/01/14
705
0
Scala下Play框架学习笔记(Body parsers)

什么是Body Parsers 一个HTTP请求是一个头部后面紧随着一个body,头部很小,可以在内存中缓存,因此Play的模型中使用了这个类。Body有时候也可能很长,以致于不能缓存,反而作为一种流而被建...

金明略
2016/12/29
0
0
【二 HTTP编程】5. Body parsers

何为Body parser? 一个HTTP请求由请求头和请求体组成。header部分通常很小 —— 因此可以在内存中被安全的缓存,在Play中对应着RequestHeader模型。相对而言,body部分可能会非常大,这时它...

Landas
2018/08/16
0
0
用Sitemesh控制页面布局

sitemesh是opensymphony团队开发的j2ee应用框架之一,旨在提高页面的可维护性和复用性。opensymphony的另一个广为人知的框架为webwork是用作web层的表示框架。他们都是开源的,可以在www.sf....

红薯
2008/11/30
1K
3

没有更多内容

加载失败,请刷新页面

加载更多

租房软件隐私保护如同虚设

近日,苏州市民赵先生向江苏新闻广播新闻热线025-84658888反映,他在“安居客”手机应用软件上浏览二手房信息,并且使用该软件自动生成的虚拟号码向当地一家中介公司进行咨询。可电话刚挂不久...

linux-tao
今天
1
0
分布式项目(五)iot-pgsql

书接上回,在Mapping server中,我们已经把数据都整理好了,现在利用postgresql存储历史数据。 iot-pgsql 构建iot-pgsql模块,这里我们写数据库为了性能考虑不在使用mybatis,换成spring jd...

lelinked
今天
4
0
一文分析java基础面试题中易出错考点

前言 这篇文章主要针对的是笔试题中出现的通过查看代码执行结果选择正确答案题材。 正式进入题目内容: 1、(单选题)下面代码的输出结果是什么? public class Base { private Strin...

一看就喷亏的小猿
今天
2
0
cocoapods 用法

cocoapods install pod install 更新本地已经install的仓库 更新所有的仓库 pod update --verbose --no-repo-update 更新制定的仓库 pod update ** --verbose --no-repo-update...

HOrange
今天
3
0
linux下socket编程实现一个服务器连接多个客户端

使用socekt通信一般步骤 1)服务器端:socker()建立套接字,绑定(bind)并监听(listen),用accept()等待客户端连接。 2)客户端:socker()建立套接字,连接(connect)服务器,连接上后...

shzwork
昨天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部