文件

§主體解析器

§什麼是主體解析器?

HTTP 要求是標頭後接主體。標頭通常很小,可以在記憶體中安全緩衝,因此在 Play 中使用 RequestHeader 類別建模。然而,主體可能會非常長,因此不會緩衝在記憶體中,而是建模為串流。但是,許多要求主體酬載很小,可以在記憶體中建模,因此為了將主體串流對應到記憶體中的物件,Play 提供 BodyParser 抽象。

由於 Play 是非同步架構,傳統的 InputStream 無法用於讀取要求主體,輸入串流會封鎖,當您呼叫 read 時,呼叫它的執行緒必須等到資料可用。相反地,Play 使用名為 Pekko Streams 的非同步串流函式庫。Pekko Streams 是 Reactive Streams 的實作,一種 SPI,允許許多非同步串流 API 無縫地協同運作,因此儘管傳統基於 InputStream 的技術不適合與 Play 搭配使用,但 Pekko Streams 和 Reactive Streams 周圍的整個非同步函式庫生態系統將提供您所需的一切。

§使用內建主體剖析器

大多數典型的網路應用程式不需要使用自訂主體剖析器,它們可以只使用 Play 的內建主體剖析器。這些剖析器包括 JSON、XML、表單剖析器,以及將純文字主體當作字串處理,將位元組主體當作 ByteString 處理。

§預設主體剖析器

如果您沒有明確選擇主體剖析器,將使用的預設主體剖析器會查看傳入的 Content-Type 標頭,並根據標頭剖析主體。例如,類型為 application/jsonContent-Type 將剖析為 JsonNode,而 application/x-www-form-urlencodedContent-Type 將剖析為 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,當累加器完成時會兌現。它基本上與 Sink<E, CompletionStage<A>> 相同,事實上它只是一個包裝器,包覆這個類型,但最大的不同是 Accumulator 提供了方便的方法,例如 mapmapFuturerecover 等,可以用來處理結果,就像它是一個承諾,而 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>> 來使用主體。然後,我們可以像承諾一樣對其進行對應,將已解析的 JsonNode 主體轉換成 User 主體。如果轉換失敗,我們會回傳 ResultLeft,說明錯誤為何

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 剖析器,它建立在 Pekko Streams 食譜中的 從 ByteStrings 串流剖析列 文件檔上

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

§延遲主體剖析

預設情況下,主體剖析會在 動作撰寫 發生之前進行。但是,有可能在動作撰寫發生之後延遲主體剖析。可以在 這裡 找到更多詳細資訊。

下一步:動作撰寫


在此文件檔中找到錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件檔指南 之後,請隨時貢獻一個 pull request。有問題或建議要分享?請前往 我們的社群論壇 與社群展開對話。