§主体解析器
§什么是主体解析器?
HTTP 请求是一个头部,后面跟着一个主体。头部通常很小 - 可以安全地缓冲在内存中,因此在 Play 中,它使用 RequestHeader
类建模。但是,主体可能非常长,因此不会缓冲在内存中,而是作为流建模。但是,许多请求主体有效负载很小,可以在内存中建模,因此为了将主体流映射到内存中的对象,Play 提供了一个 BodyParser
抽象。
由于 Play 是一个异步框架,因此传统的 InputStream
不能用于读取请求主体 - 输入流是阻塞的,当您调用 read
时,调用它的线程必须等待数据可用。相反,Play 使用一个名为 Pekko Streams 的异步流库。Pekko Streams 是 Reactive Streams 的实现,一个 SPI,它允许许多异步流 API 无缝地协同工作,因此虽然传统的基于 InputStream
的技术不适合与 Play 一起使用,但 Pekko Streams 和围绕 Reactive Streams 的整个异步库生态系统将为您提供您需要的一切。
§使用内置的正文解析器
大多数典型的 Web 应用程序不需要使用自定义正文解析器,它们可以简单地使用 Play 的内置正文解析器。这些解析器包括 JSON、XML、表单的解析器,以及将纯文本正文处理为字符串和将字节正文处理为 ByteString
的解析器。
§默认正文解析器
如果您没有显式选择正文解析器,则将使用默认正文解析器,它会查看传入的 Content-Type
标头,并相应地解析正文。例如,Content-Type
为 application/json
的类型将被解析为 JsonNode
,而 Content-Type
为 application/x-www-form-urlencoded
的类型将被解析为 Map<String, String[]>
。
可以通过 Request
上的 body()
方法访问请求正文,它被包装在一个 RequestBody
对象中,该对象提供了对正文可能存在的各种类型的便捷访问器。例如,要访问 JSON 正文
public Result index(Http.Request request) {
JsonNode json = request.body().asJson();
return ok("Got name: " + json.get("name").asText());
}
以下是默认正文解析器支持的类型的映射
text/plain
:String
,可以通过asText()
访问。application/json
:com.fasterxml.jackson.databind.JsonNode
,可以通过asJson()
访问。application/xml
、text/xml
或application/XXX+xml
:org.w3c.Document
,可以通过asXml()
访问。application/x-www-form-urlencoded
:Map<String, String[]>
,可以通过asFormUrlEncoded()
访问。multipart/form-data
:MultipartFormData
,可以通过asMultipartFormData()
访问。- 任何其他内容类型:
RawBuffer
,可以通过asRaw()
访问。
默认正文解析器尝试在尝试解析之前确定请求是否具有正文。根据 HTTP 规范,Content-Length
或 Transfer-Encoding
标头的存在表示正文的存在,因此解析器只有在其中一个标头存在时才会解析,或者在 FakeRequest
上,当显式设置了非空正文时才会解析。
如果您想在所有情况下尝试解析正文,可以使用 AnyContent
正文解析器,如 下方 所述。
§选择显式正文解析器
如果您想显式选择正文解析器,可以使用 @BodyParser.Of
注解,例如
@BodyParser.Of(BodyParser.Text.class)
public Result index(Http.Request request) {
RequestBody body = request.body();
return ok("Got text: " + body.asText());
}
Play 提供的开箱即用的正文解析器都是 BodyParser
类的内部类。简而言之,它们是
Default
: 默认正文解析器。AnyContent
: 与默认正文解析器类似,但会解析GET
、HEAD
和DELETE
请求的正文。Json
: 将主体解析为 JSON。TolerantJson
: 与Json
相似,但不验证Content-Type
标头是否为 JSON。Xml
: 将主体解析为 XML。TolerantXml
: 与Xml
相似,但不验证Content-Type
标头是否为 XML。Text
: 将主体解析为字符串。TolerantText
: 与Text
相似,但不验证Content-Type
是否为text/plain
。Bytes
: 将主体解析为ByteString
。Raw
: 将主体解析为RawBuffer
。这将尝试将主体存储在内存中,直到 Play 配置的内存缓冲区大小,但如果超过该大小,则回退到将其写入File
。FormUrlEncoded
: 将主体解析为表单。MultipartFormData
: 将主体解析为多部分表单,并将文件部分存储到文件中。Empty
: 不解析主体,而是忽略它。
应用于 WebSocket 的主体解析器将被忽略,其作用与使用 @BodyParser.Of(BodyParser.Empty.class)
相同。由于初始 WebSocket 请求不能包含主体,因此不会发生解析。
§内容长度限制
大多数内置主体解析器在内存中缓冲主体,而有些则在磁盘上缓冲主体。如果缓冲不受限制,这将打开一个潜在的漏洞,使应用程序容易受到恶意或粗心使用者的攻击。出于这个原因,Play 有两个配置的缓冲区限制,一个用于内存缓冲,另一个用于磁盘缓冲。
内存缓冲区限制使用 play.http.parser.maxMemoryBuffer
配置,默认值为 100KB,而磁盘缓冲区限制使用 play.http.parser.maxDiskBuffer
配置,默认值为 10MB。这些都可以在 application.conf
中配置,例如,将内存缓冲区限制增加到 256KB
play.http.parser.maxMemoryBuffer = 256K
您还可以通过编写自定义主体解析器来限制每个操作使用的内存量,有关详细信息,请参阅 下方。
§编写自定义主体解析器
可以通过实现 BodyParser
类来创建自定义主体解析器。此类有一个抽象方法
public abstract Accumulator<ByteString, F.Either<Result, A>> apply(RequestHeader request);
此方法的签名乍一看可能有点令人生畏,所以让我们分解一下。
该方法接受一个 RequestHeader
。这可以用来检查请求的信息 - 最常见的是,它用来获取 Content-Type
,以便正确解析请求体。
该方法的返回值类型为 Accumulator
。累加器是 Pekko Streams Sink
的一个薄层包装。累加器异步地将元素流累加到一个结果中,它可以通过传入一个 Pekko Streams Source
来运行,这将返回一个 CompletionStage
,当累加器完成时,该 CompletionStage
将被兑现。它本质上与 Sink<E, CompletionStage<A>>
相同,实际上它只是对这种类型的包装,但最大的区别是 Accumulator
提供了方便的方法,例如 map
、mapFuture
、recover
等,用于像处理 promise 一样处理结果,而 Sink
要求所有此类操作都包装在 mapMaterializedValue
调用中。
apply
方法返回的累加器消耗类型为 ByteString
的元素 - 这些本质上是字节数组,但与 byte[]
不同的是,ByteString
是不可变的,并且许多操作(如切片和追加)都在常数时间内完成。
累加器的返回值类型为 F.Either<Result, A>
。这意味着它将返回一个 Result
,或者它将返回一个类型为 A
的请求体。通常在发生错误时返回结果,例如,如果请求体解析失败,如果 Content-Type
与请求体解析器接受的类型不匹配,或者如果内存缓冲区超过了限制。当请求体解析器返回结果时,这将短路操作的处理 - 请求体解析器的结果将立即返回,并且操作将永远不会被调用。
§组合现有的请求体解析器
作为第一个示例,我们将展示如何组合现有的请求体解析器。假设您想将一些传入的 JSON 解析到您定义的类中,称为 Item
。
首先,我们将定义一个依赖于 JSON 请求体解析器的新请求体解析器
public static class UserBodyParser implements BodyParser<User> {
private BodyParser.Json jsonParser;
private Executor executor;
@Inject
public UserBodyParser(BodyParser.Json jsonParser, Executor executor) {
this.jsonParser = jsonParser;
this.executor = executor;
}
现在,在我们的 apply
方法的实现中,我们将调用 JSON 请求体解析器,它将返回 Accumulator<ByteString, F.Either<Result, JsonNode>>
来消耗请求体。然后,我们可以像处理 promise 一样对其进行映射,将解析后的 JsonNode
请求体转换为 User
请求体。如果转换失败,我们将返回一个 Left
的 Result
,说明错误是什么
public Accumulator<ByteString, F.Either<Result, User>> apply(RequestHeader request) {
Accumulator<ByteString, F.Either<Result, JsonNode>> jsonAccumulator =
jsonParser.apply(request);
return jsonAccumulator.map(
resultOrJson -> {
if (resultOrJson.left.isPresent()) {
return F.Either.Left(resultOrJson.left.get());
} else {
JsonNode json = resultOrJson.right.get();
try {
User user = play.libs.Json.fromJson(json, User.class);
return F.Either.Right(user);
} catch (Exception e) {
return F.Either.Left(
Results.badRequest("Unable to read User from json: " + e.getMessage()));
}
}
},
executor);
}
返回的正文将被包装在 RequestBody
中,可以使用 as
方法访问。
@BodyParser.Of(UserBodyParser.class)
public Result save(Http.Request request) {
RequestBody body = request.body();
User user = body.as(User.class);
return ok("Got: " + user.name);
}
§编写自定义最大长度正文解析器
另一个用例可能是定义一个正文解析器,它使用自定义的最大长度进行缓冲。许多内置的 Play 正文解析器旨在进行扩展,以允许以这种方式覆盖缓冲区长度,例如,这就是文本正文解析器可以扩展的方式
// Accept only 10KB of data.
public static class Text10Kb extends BodyParser.Text {
@Inject
public Text10Kb(HttpErrorHandler errorHandler) {
super(10 * 1024, errorHandler);
}
}
@BodyParser.Of(Text10Kb.class)
public Result index(Http.Request request) {
return ok("Got body: " + request.body().asText());
}
§将正文重定向到其他地方
到目前为止,我们已经展示了扩展和组合现有的正文解析器。有时你可能并不想解析正文,你只是想将其转发到其他地方。例如,如果你想将请求正文上传到另一个服务,你可以通过定义一个自定义正文解析器来实现这一点
public static class ForwardingBodyParser implements BodyParser<WSResponse> {
private WSClient ws;
private Executor executor;
@Inject
public ForwardingBodyParser(WSClient ws, Executor executor) {
this.ws = ws;
this.executor = executor;
}
String url = "http://example.com";
public Accumulator<ByteString, F.Either<Result, WSResponse>> apply(RequestHeader request) {
Accumulator<ByteString, Source<ByteString, ?>> forwarder = Accumulator.source();
return forwarder.mapFuture(
source -> {
// TODO: when streaming upload has been implemented, pass the source as the body
return ws.url(url)
.setMethod("POST")
// .setBody(source)
.execute()
.thenApply(F.Either::Right);
},
executor);
}
}
§使用 Pekko Streams 进行自定义解析
在极少数情况下,可能需要使用 Pekko Streams 编写自定义解析器。在大多数情况下,只需将正文缓冲在 ByteString
中即可,方法是如上所述组合 Bytes
解析器,这通常会提供一种更简单的解析方法,因为你可以使用命令式方法和对正文进行随机访问。
但是,当这不可行时,例如,当你需要解析的正文太长而无法放入内存时,你可能需要编写一个自定义正文解析器。
有关如何使用 Pekko Streams 的完整描述超出了本文档的范围 - 最好的起点是阅读 Pekko Streams 文档。但是,以下显示了一个 CSV 解析器,它基于 从 ByteStrings 流中解析行 Pekko Streams 食谱中的文档
public static class CsvBodyParser implements BodyParser<List<List<String>>> {
private Executor executor;
@Inject
public CsvBodyParser(Executor executor) {
this.executor = executor;
}
@Override
public Accumulator<ByteString, F.Either<Result, List<List<String>>>> apply(
RequestHeader request) {
// A flow that splits the stream into CSV lines
Sink<ByteString, CompletionStage<List<List<String>>>> sink =
Flow.<ByteString>create()
// We split by the new line character, allowing a maximum of 1000 characters per line
.via(Framing.delimiter(ByteString.fromString("\n"), 1000, FramingTruncation.ALLOW))
// Turn each line to a String and split it by commas
.map(
bytes -> {
String[] values = bytes.utf8String().trim().split(",");
return Arrays.asList(values);
})
// Now we fold it into a list
.toMat(
Sink.<List<List<String>>, List<String>>fold(
new ArrayList<>(),
(list, values) -> {
list.add(values);
return list;
}),
Keep.right());
// Convert the body to a Right either
return Accumulator.fromSink(sink).map(F.Either::Right, executor);
}
}
§延迟正文解析
默认情况下,正文解析发生在 动作组合 之前。但是,可以延迟正文解析,使其发生在动作组合之后。更多详细信息可以在 此处 找到。
下一步:动作组合
发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛,与社区进行交流。