后端Web框架【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址https://gitcode.com/gh_mirrors/pl/playframework点击查看免费下载HTTP 请求由头部Header与请求体Body组成头部通常很小、可以安全地缓冲在内存中而请求体可能非常大、必须以流的方式处理。Play Framework 通过BodyParser抽象把请求体字节流映射为内存中的 Scala 对象是每个Action处理请求的必经之路。本篇指南将带你理解BodyParser的设计原理、熟练使用 Play 内置的 JSON / XML / 表单 / 文件解析器并在此基础上掌握如何编写自定义解析器、限制请求体大小、以及延迟 body 解析等进阶用法让你能够精准控制每一个请求体的读取与校验方式。什么是 Body Parser在 Play 中HTTP 请求被建模为两部分头部体积小可安全地缓冲在内存中对应RequestHeader类请求体体积可能非常大因此不会整体缓冲在内存中而是被建模为一个流Stream。很多请求体实际上很小完全可以在内存中建模。为了把body 流映射为内存对象Play 提供了BodyParser抽象。由于 Play 是异步框架传统的InputStream无法用于读取请求体——输入流是阻塞的调用read时线程必须等待数据可用。取而代之Play 使用异步流式库Pekko StreamsReactive Streams SPI 的一个实现。Reactive Streams 允许众多异步流式 API 无缝协同工作因此虽然传统基于InputStream的技术不适合 Play但 Pekko Streams 以及整个 Reactive Streams 生态的异步库都能满足你的需求。从源码看BodyParser的本质非常简洁core/play/src/main/scala/play/api/mvc/Action.scala#L99trait BodyParser[A] extends (RequestHeader Accumulator[ByteString, Either[Result, A]])即给定一个RequestHeader返回一个消费ByteString流、最终产出Either[Result, A]的Accumulator。更精确地理解 Action之前我们常说Action是一个Request Result函数这并不完全准确。Actiontrait 的实际形态是文档代码示例trait Action[A] extends (Request[A] Result) { def parser: BodyParser[A] }其中泛型A是请求体的类型而Request[A]定义为文档代码示例trait Request[A] extends RequestHeader { def body: A }A可以是任何 Scala 类型——String、NodeSeq、Array[Byte]、JsValue、java.io.File等只要存在一个能处理它的 body parser 即可。总结来说Action[A]使用BodyParser[A]从 HTTP 请求中取回类型为A的值并构建出传递给 action 代码的Request[A]对象。在 Action.apply(rh: RequestHeader) 的实现中可以看到默认情况下 Play 会先运行 parserBodyParser.runParserThenInvokeAction解析完成后才调用 action 本身。使用内置的 Body Parser大多数典型 Web 应用并不需要自定义 body parser直接使用 Play 内置解析器即可。内置解析器覆盖了 JSON、XML、表单以及把纯文本 body 解析为String、把字节 body 解析为ByteString等场景。它们统一由PlayBodyParserstrait 提供可以通过依赖注入注入到控制器中。默认 Body Parser如果不显式选择 body parserPlay 会使用默认解析器它检查请求的Content-Type头并据此解析 body。例如Content-Type: application/json会被解析为JsValue而application/x-www-form-urlencoded会被解析为Map[String, Seq[String]]。默认解析器产出的 body 类型是AnyContent。AnyContent支持的各种类型通过as系列方法访问例如asJson返回 body 类型的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) } }默认解析器支持的 Content-Type 与 body 类型映射如下Content-Type解析出的类型通过as方法访问text/plainStringasTextapplication/jsonJsValueasJsonapplication/xml、text/xml、application/XXXxmlscala.xml.NodeSeqasXmlapplication/x-www-form-urlencodedMap[String, Seq[String]]asFormUrlEncodedmultipart/form-dataMultipartFormDataasMultipartFormData其他任何类型RawBufferasRaw从源码看anyContent解析器正是按Content-Type分发到text、xml、json、formUrlEncoded、multipartFormData、raw等子解析器core/play/src/main/scala/play/api/mvc/BodyParsers.scala#L941-L991。其中application/.*\xml.*类型的匹配使用正则ApplicationXmlMatcher完成BodyParsers.scala#L459所以application/atomxml、application/rssxml这类带后缀的 XML同样会被识别为 XML body。默认解析器何时真正解析默认解析器会先尝试判断请求是否真的有 body按照 HTTP 规范Content-Length或Transfer-Encoding头的存在表示请求携带 body因此只有这两个头存在或在FakeRequest上显式设置了非空 body时才会解析。这一点在源码中对应default解析器内部的request.hasBody检查BodyParsers.scala#L925-L931。如果你希望在所有情况下都尝试解析 body可以使用下面介绍的anyContent解析器。显式选择 Body Parser显式选择 body parser 的方式是把解析器传给Action的apply或async方法。例如定义一个期望 JSON body 的 action文档代码示例def save: Action[JsValue] Action(parse.json) { (request: Request[JsValue]) Ok(Got: (request.body \ name).as[String]) }注意此时 body 的类型是JsValue不再是Option操作起来更方便。原因是json解析器会校验请求的Content-Type是否为application/json或text/json不满足时直接返回415 Unsupported Media Type响应因此 action 代码里无需再次检查。源码中这一行为通过when条件解析器实现BodyParsers.scala#L691-L700def json(maxLength: Long): BodyParser[JsValue] when( _.contentType.exists(m m.equalsIgnoreCase(text/json) || m.equalsIgnoreCase(application/json)), tolerantJson(maxLength), createBadResult(Expecting text/json or application/json body, UNSUPPORTED_MEDIA_TYPE) )这当然意味着客户端必须行为良好发送正确的Content-Type头。如果希望更宽容一些可以使用tolerantJson它忽略Content-Type、无条件尝试把 body 解析为 JSON文档代码示例def save: Action[JsValue] Action(parse.tolerantJson) { (request: Request[JsValue]) Ok(Got: (request.body \ name).as[String]) }从源码看tolerantJson直接调用Json.parse(bytes.asInputStream)且刻意忽略声明的 charset——因为 JSON 的 Unicode 编码UTF-8/16/32可由前两个字节自动探测BodyParsers.scala#L664-L670。另一个例子把请求体存储到文件中文档代码示例def save: Action[File] Action(parse.file(to new File(/tmp/upload))) { (request: Request[File]) Ok(Saved the request content to request.body) }parse.file(to)的默认最大长度是磁盘缓冲上限见下文其实现本质是把 body 流通过StreamConverters.fromOutputStream写入目标文件BodyParsers.scala#L833-L845。组合 Body Parser在上面的例子中所有请求体都存到了同一个文件——这显然有问题。可以通过parse.using写一个组合式自定义解析器先从请求 Session 中提取用户名再为每个用户生成独立的文件文档代码示例val storeInUserFile parse.using { request request.session .get(username) .map { user parse.file(to new File(/tmp/ user .upload)) } .getOrElse { sys.error(You dont have the right to upload here) } } def save: Action[File] Action(storeInUserFile) { request Ok(Saved the request content to request.body) }注意这里并不是真正从零编写BodyParser而只是组合现有的解析器。大多数场景这样做就足够了完全从零编写BodyParser属于进阶话题见后文编写自定义 Body Parser一节。parse.using的特点是根据RequestHeader动态决定使用哪个解析器这正是默认解析器default内部也用到的机制BodyParsers.scala#L925。最大内容长度Max Content Length基于文本的解析器text、json、xml、formUrlEncoded等必须把全部内容加载进内存因此使用最大内容长度限制。默认情况下它们能解析的最大长度是100KB可通过在application.conf中设置play.http.parser.maxMemoryBuffer覆盖play.http.parser.maxMemoryBuffer128K对于把内容缓冲到磁盘的解析器如 raw 解析器或multipart/form-data最大长度由play.http.parser.maxDiskBuffer指定默认10MB。multipart/form-data解析器还会对所有 data 字段的总和强制执行文本最大长度限制——从源码看它在内部调用Multipart.multipartParser(DefaultMaxTextLength, ...)即 text 部分仍受内存缓冲上限约束BodyParsers.scala#L1058-L1062。这些默认值来自ParserConfigurationcase class ParserConfiguration( maxMemoryBuffer: Long 102400, maxDiskBuffer: Long 10485760, allowEmptyFiles: Boolean false )配置读取位于 HttpConfiguration.scala#L234-L239maxMemoryBuffer使用getDeprecatedConfigMemorySize读取旧键名parsers.text.maxLength仍然兼容maxDiskBuffer与allowEmptyFiles则直接读取play.http.parser.maxDiskBuffer和play.http.parser.allowEmptyFiles。针对单个 action 覆盖默认上限直接给解析器传maxLength参数文档代码示例// Accept only 10KB of data. def save: Action[String] Action(parse.text(maxLength 1024 * 10)) { (request: Request[String]) Ok(Got: text) }用maxLength包装任意解析器maxLength是PlayBodyParsers提供的方法它把任意 body parser 包装成带大小上限的版本当超过限制时返回Left(MaxSizeExceeded)文档代码示例// 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) }从源码看enforceMaxLength使用了一个名为TakeUpTo的自定义 Pekko StreamsGraphStage来计数流入的字节数一旦累计字节数超过maxLength立即以MaxSizeExceeded(maxLength)完成状态并让流失败MaxLengthLimitAttained从而阻止下游解析器继续解析超限数据BodyParsers.scala#L1072-L1088、BodyParsers.scala#L1166-L1222。此外如果请求头里已有Content-Length且超过上限解析器会直接返回413 Request Entity Too Large连流都不会开始消费BodyParsers.scala#L414-L418。编写自定义 Body Parser自定义 body parser 通过实现BodyParsertrait 完成它本质上就是一个函数文档代码示例trait BodyParser[A] extends (RequestHeader Accumulator[ByteString, Either[Result, A]])这个签名初看有些吓人逐项拆解如下入参RequestHeader用于检查请求信息——最常见的是读取Content-Type以便正确解析 body。也可以读取 Session、Headers 等做更精细的分发。返回值Accumulator[ByteString, Either[Result, A]]Accumulator是 Pekko StreamsSink的薄封装本质等价于Sink[E, Future[A]]。向它传入一个 Pekko StreamsSource即可运行运行结束会返回一个被Future兑现的结果。Accumulator与普通Sink最大的区别是它提供了map、mapFuture、recover、recoverWith等便捷方法让你可以像操作 promise 一样直接处理结果而Sink中这类操作都必须包裹在mapMaterializedValue调用里。相关定义见 core/play-streams/src/main/scala/play/api/libs/streams/Accumulator.scala#L30-L51。消费的元素ByteString本质上是字节数组但ByteString是不可变的且切片、追加等操作都是常数时间。产出类型Either[Result, A]要么返回Result要么返回类型为A的 body。返回Result通常意味着出错——例如 body 解析失败、Content-Type与解析器期望不匹配、或内存缓冲超限。当 body parser 返回Result时action 的处理会被短路parser 的结果被立即返回action 永远不会被调用参见 Action.scala#L253-L259 中runParserThenInvokeAction对Left(r)的处理。把 body 转发到别处编写 body parser 的一个常见场景是你其实不想解析body而是想把它流式转发到别处例如用 WSClient 把请求体原样 POST 给另一个服务。此时可以利用Accumulator.source文档代码示例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) } }Accumulator.source[ByteString]会把上游流入的ByteString流转换为一个 Pekko StreamsSource并作为累积结果产出Accumulator.scala#L276-L283随后该Source被直接交给 WSClient 作为上传内容——整个过程零拷贝、全流式body 无需落地到内存或磁盘。基于 Pekko Streams 自定义解析极少数情况下可能需要用 Pekko Streams 编写真正的自定义解析器。大多数场景下先缓冲整个 body 为ByteString就足够了因为可以用命令式方法和随机访问来解析简单得多。但当 body 太长、无法放入内存时例如要处理超大文件流就需要编写流式自定义解析器。Pekko Streams 的完整用法超出本文范围下面展示一个 CSV 解析器示例它构建自从 ByteString 流中解析行的经典模式文档代码示例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) }要点拆解Framing.delimiter(ByteString(\n), 1000, allowTruncation true)负责按换行符切分字节流每行最多 1000 字节允许最后一行没有结尾换行符map(_.utf8String.trim.split(,).toSeq)把每一行字节解码为String并按逗号切分成Seq[String]Sink.fold把所有行累积成Seq[Seq[String]]最后用Accumulator(sink).map(Right.apply)把累积结果包装成Either[Result, Seq[Seq[String]]]的右值。该示例在仓库中有对应的测试用例code/ScalaBodyParsers.scala#L158-L186向该解析器发送1,2\n3,4,foo\n5,6后req.body(1)(2)取出的正是foo。延迟 Body 解析Deferred Body Parsing默认情况下body 解析发生在 action 组合Action Composition 之前。但也可以把 body 解析推迟到action 组合定义的部分或全部action 处理完成之后。从源码看这一机制由请求属性RequestAttrKey.DeferredBodyParsing驱动Action.apply(rh: RequestHeader)会检查该属性若存在则跳过解析、直接用空 body 运行 actionAction.scala#L73-L83之后可通过BodyParser.parseBody在合适的时机真正执行解析——解析完成后调用next且由于移除了该请求属性多次调用也不会重复解析Action.scala#L223-L243。具体的使用场景与示例请阅读 ScalaActionsComposition.md 中的 Action composition in interaction with body parsing 一节那里详细解释了如何在 action 组合中与 body 解析交互、以及何时应该选择延迟解析。小结与进一步阅读BodyParser是 Play 异步 HTTP 处理流水线中承上启下的关键一环它把请求体字节流安全地、异步地转换为类型化的 Scala 值并通过Either[Result, A]在解析失败时优雅地短路 action。日常开发中parse.json、parse.tolerantJson、parse.xml、parse.formUrlEncoded、parse.multipartFormData、parse.file、parse.raw等内置解析器配合maxLength与play.http.parser.*配置即可覆盖绝大多数需求需要定制时parse.using组合、Accumulator.source转发、以及基于 Pekko Streams 的流式解析如 CSV 示例提供了从易到难的三级进阶路径。与本主题相关的仓库资源核心 API 定义core/play/src/main/scala/play/api/mvc/Action.scalaAction、BodyParsertrait内置解析器全集core/play/src/main/scala/play/api/mvc/BodyParsers.scalaPlayBodyParsers、BodyParsers、MaxSizeExceeded解析器配置项core/play/src/main/scala/play/api/http/HttpConfiguration.scalaParserConfiguration流累积器实现core/play-streams/src/main/scala/play/api/libs/streams/Accumulator.scala可运行示例与测试documentation/manual/working/scalaGuide/main/http/code/ScalaBodyParsers.scala相关主题ScalaActionsComposition.mdaction 组合与延迟解析赞分享后端Web框架【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址https://gitcode.com/gh_mirrors/pl/playframework点击查看免费下载相关推荐Kilo Code 开源 AI 编程代理实战指南跨 VS Code、JetBrains 与 CLI 的安装方式、内置 Agent 与 CI/CD 自主模式全解析Kilo Code 开源 AI 编程代理实战指南跨 VS Code、JetBrains 与 CLI 的安装方式、内置 Agent 与 CI/CD 自主模式全解后端Web框架Django REST Framework Parsers 完全指南媒体类型解析、内置解析器与自定义解析器实战Django REST Framework Parsers 完全指南媒体类型解析、内置解析器与自定义解析器实战 Django REST FrameworkD后端API网关Web框架Play Framework 编写 Play Modules 完全指南从自定义模块到覆盖内置模块Play Framework 编写 Play Modules 完全指南从自定义模块到覆盖内置模块 Play Framework 的模块Module机制是扩后端Web框架上一篇resnet18.fb_swsl_ig1b_ft_in1k迁移学习指南如何快速微调到自定义数据集下一篇Wire与持续部署CD流水线的依赖管理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?