文件

§本體剖析器

§什麼是本體剖析器?

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

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

§更多關於 Action

先前我們說過 ActionRequest => 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 類型是要求主體的類型。我們可以使用任何 Scala 類型作為要求主體,例如 StringNodeSeqArray[Byte]JsonValuejava.io.File,只要我們有能夠處理它的主體剖析器即可。

總之,Action[A] 使用 BodyParser[A] 從 HTTP 要求中擷取 A 類型的值,並建立傳遞給動作程式碼的 Request[A] 物件。

§使用內建主體剖析器

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

§預設主體剖析器

如果您沒有明確選擇主體剖析器,則使用的預設主體剖析器會查看傳入的 Content-Type 標頭,並相應地剖析主體。例如,Content-Type 類型的 application/json 將被剖析為 JsValue,而 Content-Type 類型的 application/x-www-form-urlencoded 將被剖析為 Map[String, Seq[String]]

預設主體剖析器產生 AnyContent 類型的主體。AnyContent 支援的各種類型可透過 as 方法存取,例如 asJson,它會傳回主體類型的 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")
    }
}

以下是預設主體剖析器支援的類型對應

預設主體剖析器會在嘗試剖析之前,試著判斷要求是否具有主體。根據 HTTP 規格,Content-LengthTransfer-Encoding 標頭的存在表示存在主體,因此剖析器僅會在存在其中一個標頭時剖析,或在 FakeRequest 上,當明確設定非空主體時。

如果您想在所有情況下嘗試剖析主體,可以使用 anyContent 主體剖析器,說明如下 下方

§選擇明確的主體剖析器

如果您想明確選擇主體剖析器,可以透過將主體剖析器傳遞給 Action applyasync 方法來完成。

Play 提供許多現成的主體剖析器,這可透過 PlayBodyParsers 特性取得,可以將其注入控制器中。

因此,例如,要定義一個期待 json 主體的動作(如前一個範例)

def save: Action[JsValue] = Action(parse.json) { (request: Request[JsValue]) =>
  Ok("Got: " + (request.body \ "name").as[String])
}

這次請注意,主體的類型是 JsValue,由於不再是 Option,因此更容易處理主體。其之所以不是 Option,是因為 json 主體剖析器會驗證要求的 Content-Type 是否為 application/json,如果要求不符合此預期,則會傳回 415 不支援的媒體類型 回應。因此,我們不需要在動作程式碼中再次檢查。

這當然表示客戶端必須表現良好,在要求中傳送正確的 Content-Type 標頭。如果您想放寬一點,可以使用 tolerantJson,它會忽略 Content-Type 並嘗試將主體剖析為 json

def save: Action[JsValue] = Action(parse.tolerantJson) { (request: Request[JsValue]) =>
  Ok("Got: " + (request.body \ "name").as[String])
}

以下是另一個範例,它會將要求主體儲存在檔案中

def save: Action[File] = Action(parse.file(to = new File("/tmp/upload"))) { (request: Request[File]) =>
  Ok("Saved the request content to " + request.body)
}

§結合主體剖析器

在先前的範例中,所有要求主體都儲存在同一個檔案中。這有點問題,不是嗎?讓我們撰寫另一個自訂主體剖析器,從要求 Session 中萃取使用者名稱,以提供每個使用者一個獨特的文件

val storeInUserFile = parse.using { request =>
  request.session
    .get("username")
    .map { user => parse.file(to = new File("/tmp/" + user + ".upload")) }
    .getOrElse {
      sys.error("You don't have the right to upload here")
    }
}

def save: Action[File] = Action(storeInUserFile) { request => Ok("Saved the request content to " + request.body) }

注意:這裡我們並未真正撰寫我們自己的 BodyParser,而只是結合現有的。這通常就夠了,而且應該涵蓋大多數使用案例。從頭開始撰寫 BodyParser 會在進階主題部分中說明。

§最大內容長度

基於文字的主體剖析器(例如 textjsonxmlformUrlEncoded)使用最大內容長度,因為它們必須將所有內容載入記憶體中。預設情況下,它們會剖析的最大內容長度為 100KB。它可以透過在 application.conf 中指定 play.http.parser.maxMemoryBuffer 屬性來覆寫

play.http.parser.maxMemoryBuffer=128K

對於將內容緩衝到磁碟的剖析器,例如原始剖析器或 multipart/form-data,最大內容長度是使用 play.http.parser.maxDiskBuffer 屬性指定的,預設為 10MB。multipart/form-data 剖析器也會對資料欄位的總和強制執行文字最大長度屬性。

您也可以為特定動作覆寫預設最大長度

// Accept only 10KB of data.
def save: Action[String] = Action(parse.text(maxLength = 1024 * 10)) { (request: Request[String]) =>
  Ok("Got: " + text)
}

您也可以用 maxLength 包裝任何主體剖析器

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

§撰寫自訂主體剖析器

可透過實作 BodyParser 特質來建立自訂主體剖析器。此特質只是一個函式

trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])

此函式的簽章乍看之下可能有點嚇人,因此我們來分解它。

此函式會使用 RequestHeader。這可用於檢查要求資訊 - 最常見的是用於取得 Content-Type,以便正確剖析主體。

此函式的回傳類型是 Accumulator。累加器是 Pekko 串流 Sink 周圍的一層薄薄的包裝。累加器會非同步地將元素串流累加到結果中,它可以透過傳入 Pekko 串流 Source 來執行,這會回傳一個 Future,當累加器完成時會兌現。它基本上與 Sink[E, Future[A]] 相同,事實上它只是一個包裝在這個類型周圍的包裝器,但最大的不同是 Accumulator 提供了方便的方法,例如 mapmapFuturerecover 等,可將結果當作承諾來使用,而 Sink 則需要將所有此類操作包裝在 mapMaterializedValue 呼叫中。

apply 方法回傳的累加器會使用 ByteString 類型的元素 - 它們基本上是位元組陣列,但與 byte[] 的不同之處在於 ByteString 是不可變的,而且許多操作(例如切片和附加)會在固定時間內發生。

累加器的回傳類型是 Either[Result, A] - 它會回傳 ResultA 類型的主體。結果通常會在發生錯誤時回傳,例如,如果主體剖析失敗、Content-Type 與主體剖析器接受的類型不符,或超過記憶體緩衝區時。當主體剖析器回傳結果時,這會短路動作的處理 - 主體剖析器結果會立即回傳,而且動作永遠不會被呼叫。

§將主體導向其他地方

撰寫主體解析器的一個常見使用案例是,當你實際上不想解析主體時,而是想將它串流到其他地方。為執行此操作,你可以定義自訂主體解析器

import javax.inject._

import scala.concurrent.ExecutionContext

import org.apache.pekko.util.ByteString
import play.api.libs.streams._
import play.api.libs.ws._
import play.api.mvc._

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

§使用 Pekko Streams 進行自訂解析

在罕見情況下,可能需要使用 Pekko Streams 撰寫自訂解析器。在大部分情況下,先將主體緩衝在 ByteString 中就已足夠,這通常會提供更簡單的解析方式,因為你可以對主體使用命令式方法和隨機存取。

不過,當這不可行時,例如當你需要解析的主體過長而無法放入記憶體時,你可能需要撰寫自訂主體解析器。

如何使用 Pekko Streams 的完整說明超出本文件說明的範圍 - 最好的入門方式是閱讀 Pekko Streams 文件。不過,以下顯示一個 CSV 解析器,它建立在 從 ByteStrings 串流解析行 的 Pekko Streams 食譜文件說明上

import org.apache.pekko.stream.scaladsl._
import org.apache.pekko.util.ByteString
import play.api.libs.streams._
import play.api.mvc.BodyParser

val Action = inject[DefaultActionBuilder]

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

§延遲主體解析

預設情況下,主體解析會在 動作組成 發生之前進行。不過,有可能在透過動作組成定義的部分 (或全部) 動作處理之後,延遲主體解析。可以在 此處 找到更多詳細資料。

下一步:動作組成


在這份文件中發現錯誤?此頁面的原始程式碼可以在 此處 找到。在閱讀 文件指南 之後,請隨時提出拉取請求。有問題或建議要分享?請前往 我們的社群論壇,與社群展開對話。