【二 HTTP編程】5. Body parsers 原

何爲Body parser?

一個HTTP請求由請求頭和請求體組成。header部分通常很小 —— 因此可以在內存中被安全的緩存,在Play中對應着RequestHeader模型。相對而言,body部分可能會非常大,這時它不會直接緩存在內存中,而是以流的形式來處理。但是許多請求的請求體也會很小, 可以直接映射到內存,爲了將請求體的流看做一個內存對象,Play提供了BodyParser抽象。

Play作爲一個異步框架,無法使用傳統的InputStream來讀取請求體的流——因爲InputStream是阻塞的,當你調用read方法時,調用此方法的的線程必須等待數據到達並可用。作爲替代,Play提供了一個異步的流處理庫——Akka Streams。Akka流是Reactive Stream的實現,一個允許多個異步API無縫集成的SPI。記住基於InputStream的技術在Play中是不適用的,Akka Stream異步庫及其完整的生態環境將提供你需要的全部。

關於Actions

前面我們說過Action是一個 Request => Result 函數。這個說法並不完全正確,我們先來看看Action:

trait Action[A] extends (Request[A] => Result) {
    def parser: Parser[A]
}

首先能看到類定義中的泛型A,然後一個action必須定義一個BodyParser[A]。相應的Request[A]定義如下:

trait Request[+A] extends RequestHeader {
    def body: A
}

A類型即請求體的類型。我們可以使用任意Scala類型作爲請求體,如: String,NodeSeq,Array[Byte],JsonValue,或者java.io.File,只要有相應的body parser來處理它。

總結一下,Action[A]使用了一個BodyParser[A]來從HTTP請求中獲取類型A,然後創建一個Request[A]對象並將它傳給action代碼。

使用內置的body parsers

大多數的常見的web apps不需要自定義新的body parsers,只需要使用Play內置的body parser就可以工作的很好。包括 JSON,XML,forms及普通文本格式的body體(String)、二進制的body體(ByteString)。

默認的body parser

如果沒有顯式的指定一個body parser,Play將會根據 Content-Type 選擇一個對應的body parser。如,一個Context-Type爲application/json將會當做JsValue處理,而application/x-www-from-unlencoded 將會被處理爲 Map[String, Seq[String]]

默認的parser將會創建一個類型爲 AnyContent 的body,AnyContext中的可變可變由 as 方法指定,如asJson將會返回一個Option類型的body:

def save = 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")
    }
}

下面是一張默認body parser的映射列表:

  • text/plain:String,對應asText
  • application/json:JsValue,對應 asJson
  • application/xml,text/xml 或 application/XXX+xml:scala.xml.NodeSeq,對應asXml
  • application/x-www-form-urlencoded:Map[String, Seq[String]],對應 asFormUrlEncoded
  • multipart/form-data:MultipartFormData,對應asMultipartFormData。
  • 任意其他類型:RawBuffer,對應 asRaw

默認的body parser在解析前會判斷request是否包含了body。HTTP規範規定了 Content-Length / Transfer-Encoding 示意了請求中會帶body,因此parser僅在請求提供了這些頭時纔會解析,還有一種情況就是在 FakeRequest 中明確設置了非空body。

如果你希望在任意情況下都解析body,你可以嘗試使用下文中提到的 anyContent body parser。

指定body parser

如果你希望顯式指定一個body parser,可以通過調用Action 的 apply 或 async 方法,向其傳遞一個 body parser。

Play提供了一系列開箱即用的body parser,他們都繼承了PlayBodyParsers特質,可以直接注入到controller。

一個處理json body的action示例如下:

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

注意這裏的body類型爲JsValue而非Option,因此變得更加容易處理。內部的機制是json body parser將校驗請求是否有application/json的Content-Type,如果沒有,將直接返回415 Unsupported Media Type。所以我們的代碼中無需再次檢測。

這意味着提高了對客戶端代碼的規範要求,必須保證他們的Content-Type被正確設置。如果你想放鬆要求,可以使用 tolerantJson方法,它會忽略Content-Type,並努力嘗試解析body。

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

下面是一個將request body寫入文件的例子:

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

組合body parsers

前面的例子中,所有request bodies全部存儲在同一個文件中。下面我們來從request中解析用戶名,來爲每個用戶創建單獨的文件:

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(storeInUserFile) { request =>
    Ok("Saved the request content to " + request.body)
}

注意:這並不是寫一個新的parser,而是組合了已有parser。這種方式已足以應付大多數情況。關於如何從零開始自定義一個BodyParser將在高級主題中講述。

最大內容長度

基於文本的body parsers(包括 textjsonxml 或者 formUrlEncoded)使用了 最大內容長度 (max content length),因爲它們需要將整個content載入內存。默認情況下,最大的content length是100kb。這個值可以通過設置application.conf中的play.http.parser.maxMemoryBuffer來重新定義:

play.http.parser.maxMemoryBuffer=128K

有些parser會將內容緩存在硬盤上,如 raw parser 或者 multipart/form-data,最大content length由 play.http.parser.maxDiskBuffer定義,默認值是10MB。multipart/form-data parser還會數據字段的聚合強制使用 text max length 屬性。

你可以在指定action中覆蓋這個設置:

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

你還可以爲任意的body parser指定maxLength:

// Accept only 10KB of data.
def save = Action(parse.maxLength(1024 * 10, storeInUserFile)){ request =>
    Ok("Saved the request content to " + request.body)
}

自定義body parser

你可以通過實現BodyParser特質來自定義一個body parser。BodyParser是一個簡單地函數:

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

這個函數的簽名看起來有點嚇人,所以下面一起來分解。

函數接受一個RequestHeader。它將被用來檢查request信息 —— 大多數情況下它將檢查 Content-Type,來保證body以正確的格式被解析。

函數的返回值類型是 Accumulator。累加器(accumulator)是對Akka Streams Sink的簡單封裝。累加器異步的將元素流累積到result中,它可以通過傳入Akka Streams Source 來運行,並返回一個Future指示累加器的完成狀態。本質上它和Sink[E, Future[A]]是一樣的,事實上也的確如此,它就是在其上的一層封裝。不同之處在於Accumulator提供了一系列有用的方法,如map,mapFuture,recover等等,將result視爲一個promise來操作。Sink要求所有這些操作都包含在mapMaterializedValue調用中。

累加器的apply方法返回的是 ByteString 類型 —— 其實就是bytes數組,不同之處是ByteString是不可變的,並且以固定時間耗費提供了 slicing、appending 等操作。
累加器的返回值是 Either[Result, A] —— 即返回一個Result,或者一個A類型的body。result一般是在發生錯誤時返回,如者 body parser 不接受此Content-Type類型導致解析失敗,或者超出了內存中的緩存大小限制。當body parser返回一個result時,它將此action短路 —— body parser立即返回,action將不會被調用。

重定向body

一個寫body parser的常見例子是你不想處理此body,而是想將它引到其它地方。你可以這樣定義你的parser:

import javax.inject._
import play.api.mvc._
import play.api.libs.streams._
import play.api.libs.ws._
import scala.concurrent.ExecutionContext
import akka.util.ByteString

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()
        .map(Right.apply)
    }
  }

  def myAction = Action(forward(ws.url("https://example.com"))) { req =>
    Ok("Uploaded")
  }
}

使用Akka Streams來自定義解析

在極少數情況下,你可能需要使用到Akka Streams。大多數情況下你可以將body緩存到一個ByteString中,這樣會使操作簡單很多,而且提供了對body的隨機訪問。

但是,當你需要處理很長的body時你就無法將它整個放入內存。

如何使用Akka Streams已經超出了本文檔的講述範圍。你可以移步這裏查看 Akka Streams 的細節。我們下面提供了一個CSV解析器的簡單例子,它基於Akka Streams cookbook 的 Parsing lines from a stream of ByteStrings 部分:

import play.api.mvc.BodyParser
import play.api.libs.streams._
import akka.util.ByteString
import akka.stream.scaladsl._

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章