Body parsers[翻译]

栏目: Scala · 发布时间: 7年前

内容简介:Body parsers[翻译]

什么是Body解析器?

一个HTTP请求是一个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)  
}

原文: Body parsers


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Understanding Machine Learning

Understanding Machine Learning

Shai Shalev-Shwartz、Shai Ben-David / Cambridge University Press / 2014 / USD 48.51

Machine learning is one of the fastest growing areas of computer science, with far-reaching applications. The aim of this textbook is to introduce machine learning, and the algorithmic paradigms it of......一起来看看 《Understanding Machine Learning》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具