文档

§主体解析器

§什么是主体解析器?

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-Typeapplication/json 的类型将被解析为 JsonNode,而 Content-Typeapplication/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());
}

以下是默认正文解析器支持的类型的映射

默认正文解析器尝试在尝试解析之前确定请求是否具有正文。根据 HTTP 规范,Content-LengthTransfer-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 类的内部类。简而言之,它们是

应用于 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 提供了方便的方法,例如 mapmapFuturerecover 等,用于像处理 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 请求体。如果转换失败,我们将返回一个 LeftResult,说明错误是什么

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);
  }
}

§延迟正文解析

默认情况下,正文解析发生在 动作组合 之前。但是,可以延迟正文解析,使其发生在动作组合之后。更多详细信息可以在 此处 找到。

下一步:动作组合


发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛,与社区进行交流。