Body parsers【翻译】

原文: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)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,373评论 0 17
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,602评论 18 399
  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,650评论 24 1,002
  • { "Unterminated string literal.": "未终止的字符串文本。", "Identifi...
    一粒沙随风飘摇阅读 10,518评论 0 3