標籤:

共用的抽象模型(客戶及伺服器端)- (編集/反編集)過程

(編集/反編集)過程

「編集過程」 (「Marshalling」) 是指一個將高階(對象)結構轉化為某種低階表現模式(例如 xml/json 等通訊或儲存格式)的過程。也有其它比較流行叫法如「序列化」 (「Serialization」/Java 或 "Pickling" / Python )

而「反編集過程」 (「UnMarshalling」) 是指一個將某種低階表現模式(例如 xml/json 等通訊或儲存格式)轉化為高階(對象)結構的過程。也有其它比較流行叫法如「反序列化」 (「Deserialization」/Java 或 "Unpickling" / Python )

(譯註:原文這裡是分兩章寫的,然而文字重複內容太多,而且兩者放在一起讀反而更好理解。所以斗膽了...)

(譯註:原文開篇其實有點越說越亂的感覺,因為 Marshalling 雖然在語義上最接近序列化,但這個詞在 C# 中的序列化這個含義並不強烈,比較通用在各種需要轉化數據的場景,而 Java 一般在 RMI 的時候才會用這個詞,而 Python 則完全避開這個詞而用「腌制」。而在 Akka HTTP 則更多用在類型轉換間,而且是非預設類型 T 與某些特定類型的轉換。很坑...)

在 Akka HTTP 「Marshalling「 意味者把一個 T 類型的對象轉化為低階對象類型,如 MessageEntity 或者 HttpRequest 又或 HttpResponse。而 「Unmarshalling」 則是這個過程的逆轉。

基礎設計

由 A 類型編集成 B 類型的實例的過程一般由一個 Marshaller[A, B] 編集器完成。而反編集的過程則是由 Unmarshaller[A, B] 處理。Akka HTTP 已經預設了一系列的編集/反編集器類型別名以便開發者使用。

(譯註,不熟悉 類型別名 (type alias)的讀者可以參考相關的 Scala Python C++ 概念)

編集類型別名

type ToEntityMarshaller[T] = Marshaller[T, MessageEntity]ntype ToByteStringMarshaller[T] = Marshaller[T, ByteString]ntype ToHeadersAndEntityMarshaller[T] = Marshaller[T, (immutable.Seq[HttpHeader], MessageEntity)]ntype ToResponseMarshaller[T] = Marshaller[T, HttpResponse]ntype ToRequestMarshaller[T] = Marshaller[T, HttpRequest]n

反編集類型別名

type FromEntityUnmarshaller[T] = Unmarshaller[HttpEntity, T]ntype FromMessageUnmarshaller[T] = Unmarshaller[HttpMessage, T]ntype FromResponseUnmarshaller[T] = Unmarshaller[HttpResponse, T]ntype FromRequestUnmarshaller[T] = Unmarshaller[HttpRequest, T]ntype FromByteStringUnmarshaller[T] = Unmarshaller[ByteString, T]ntype FromStringUnmarshaller[T] = Unmarshaller[String, T]ntype FromStrictFormFieldUnmarshaller[T] = Unmarshaller[StrictForm.Field, T]n

與開發者可能猜測的不一樣,Marshaller[A, B] 並不是一個單純的 A => B 函數,而是一個相對複雜的 A => Future[List[Marshalling[B]]] 函數。 這裡我們可以把這看起來比較複雜的函數簽名,拆開一件一件地讀解一下相關編集器是如何設計的。例如對應類型 A 的一個 組編器 Marshaller[A, B]:

一個 Future,這個可能相對比較清晰。編集器不一定要同步產生一個結果,所以返回一個 Future,以便編集過程可以非同步完成。

(譯註,其實...這個前提並不是那麼清晰,這個只能說設計者認為這個過程比較費時,希望程序能盡量不阻塞在這個過程上,雖然這是 CPU 偏重的運行過程)

List:與其返回一個單一的表達類型,類型 A 的編集器可以提供幾個不同的目標類型。具體哪個類型作為最終渲染用到通訊渠道上,則依賴於雙方內容協商。例如,ToEntityMarshaller[OrderConfirmation] 可能既提供 JSON 又提供 XML。客戶可以通過增加一個 Accept 請求頭域確認到底要哪一種。如果客戶端不具體聲明,那麼就會默認為 List 里的的第一種。

Marshalling[B]:與其直接返回一個 B 目標類型的實例,編集器會先返回一個 Marshalling[B] 類型。這使編集器可以在進入編集過程先查詢確認 MediaTypeHttpCharset。這樣的設計既可以支持內容協商,又可以使具體的目標編集對象推遲到有需要的時候才被構建。

Marshalling 類型定義如下:

/**n * 對應一個值,一種可能性n */nsealed trait Marshalling[+A] {n def map[B](f: A ? B): Marshalling[B]n}nnnobject Marshalling {nn /**n * 對應特定的 [akka.http.scaladsl.model.ContentType] 類型的 Marshallingn */n final case class WithFixedContentType[A](n contentType: ContentType,n marshal: () ? A) extends Marshalling[A] {n def map[B](f: A ? B): WithFixedContentType[B] = copy(marshal = () ? f(marshal()))n }nn /**n * 對應特定的 [akka.http.scaladsl.model.ContentType] 類型的 Marshallingn */n final case class WithOpenCharset[A](n mediaType: MediaType.WithOpenCharset,n marshal: HttpCharset ? A) extends Marshalling[A] {n def map[B](f: A ? B): WithOpenCharset[B] = copy(marshal = cs ? f(marshal(cs)))n }nn /**n * 對應未知的 MediaType 和 charset 的 Marshalling,以便處理內容協商n */n final case class Opaque[A](marshal: () ? A) extends Marshalling[A] {n def map[B](f: A ? B): Opaque[B] = copy(marshal = () ? f(marshal()))n }n}n

Unmarshaller[A, B] 的本質上與其對應的編集器是一樣的,甚至更簡單些。反編集的過程並不需要處理內容協商過程,因此節省了編集過程中需要的兩層額外的數據處理。

系統預設的編集/反編集器

(譯註:這裡再次強調一下 Marshalling 在 Akka HTTP 裡面更多的是一個轉換過程,因為在大家一般的認知上,String 應該是比 HttpEntity 」低階「的對象,然而,在程序本身的模型上,HttpEntity 類型是 Akka HTTP 通訊抽象的低階,而 String 和 開發者自己定義的 例如 Person 則是一個級別的抽象度 T)

Akka HTTP 已經預先為主流的類型設定了一系列的編集器。它們是:

預設的 ToEntityMarshallers

  • Array[Byte]

  • ByteString
  • Array[Char]
  • String

  • akka.http.scaladsl.model.FormData

  • akka.http.scaladsl.model.MessageEntity

  • T <: akka.http.scaladsl.model.Multipart

預設的 ToResponseMarshallers

  • T, 如果有現成的 ToEntityMarshaller[T]

  • HttpResponse StatusCode

  • (StatusCode, T), 如果有現成的 ToEntityMarshaller[T]

  • (Int, T), 如果有現成的 ToEntityMarshaller[T]

  • (StatusCode, immutable.Seq[HttpHeader], T), 如果有現成的ToEntityMarshaller[T]

  • (Int, immutable.Seq[HttpHeader], T), 如果有現成的 ToEntityMarshaller[T]

預設的 ToRequestMarshallers

  • HttpRequest

  • Uri (HttpMethod, Uri, T), 如果有現成的 ToEntityMarshaller[T]

  • (HttpMethod, Uri, immutable.Seq[HttpHeader], T), 如果有現成的ToEntityMarshaller[T]

通用型 Marshallers

  • Marshaller[Throwable, T]

  • Marshaller[Option[A], B], 如果有現成的 Marshaller[A, B] 和 EmptyValue[B]
  • Marshaller[Either[A1, A2], B], 如果有現成的 Marshaller[A1, B] 和 Marshaller[A2, B]

  • Marshaller[Future[A], B], 如果有現成的 Marshaller[A, B]

  • Marshaller[Try[A], B], 如果有現成的 Marshaller[A, B]

以及反編集器。它們是:

預設的 FromStringUnmarshallers

  • Byte

  • Short Int

  • Long

  • Float

  • Double

  • Boolean

預設的 FromEntityUnmarshallers

  • Array[Byte]

  • ByteString Array[Char]

  • String

  • akka.http.scaladsl.model.FormData

通用型 Unmarshallers

  • Unmarshaller[T, T] (identity unmarshaller)

  • Unmarshaller[Option[A], B], 如果有現成的 Unmarshaller[A, B]
  • Unmarshaller[A, Option[B]], 如果有現成的 Unmarshaller[A, B]

隱式處理

Akka HTTP 編集/反編集過程的基礎設施實現基於 Scala 的類型類模式,這意味著的從 A 類型到 B 類型的編集/反編集器的實例必須作為一個隱式參數/函數存在。

(譯註,Scala 的 type-class 類型類實現的有些怪異,從 Haskell 過來的讀者可能有點會被帶坑,不過還是建議兩者都對比看看)

大部分 Akka HTTP 里預設的編集/反編集器隱式工具都在 Marshaller/Unmarshaller trait 的伴生對象里。這意味著它們已經隨手待用,而不需要特意導入。另外,開發者隨時可以在本地可視域里用自己的版本覆蓋原有版本。

客制化編集/反編集器

Akka HTTP 提供了幾個小工具給開發者構建自己所需類型的編集/反編集器。不過在動手前,開發者需要考慮清楚到底需要具體什麼類型的編集/反編集器,如果開發者的編集器只是需要生成一個 MessageEntity 類型,那麼其實開發者應該準備的是一個 ToEntityMarshaller[T]。使用這個編集器類型的優點是,可以應用與客戶和伺服器端兩種場合,因為如果只要有一個 ToEntityMarshaller[T] 存在的話 ToResponseMarshaller[T]ToRequestMarshaller[T] 就會被自動生成。

但是如果開發者的器器同時需要去配置其它內容如響應狀態代碼,請求方式,請求 URI 或者任意某頭域,那麼 ToEntityMarshaller[T] 就意義不大了。開發者必須細化一點,直接提供一個 ToResponseMarshaller[T] 或者 ToRequestMarshaller[T]

反編集過程同理。

在編寫開發者客所需的編集/反編集器時,開發者無需「手工」實現 Marshaller/Unmarshaller trait。因為很有可能使用 Marshaller/Unmarshaller 伴生對象里的一些輔助型的構造函數就能做到。

在 Marshaller 中:

object Marshallern extends GenericMarshallersn with PredefinedToEntityMarshallersn with PredefinedToResponseMarshallersn with PredefinedToRequestMarshallers {nn /**n * 用提供的函數生成一個 [[Marshaller]] n */n def apply[A, B](f: ExecutionContext ? A ? Future[List[Marshalling[B]]]): Marshaller[A, B] =n new Marshaller[A, B] {n def apply(value: A)(implicit ec: ExecutionContext) =n try f(ec)(value)n catch { case NonFatal(e) ? FastFuture.failed(e) }n }nn /**n * 輔助函數以便用提供的函數生成一個 [[Marshaller]] n */n def strict[A, B](f: A ? Marshalling[B]): Marshaller[A, B] =n Marshaller { _ ? a ? FastFuture.successful(f(a) :: Nil) }nn /**n * 輔助函數以便用多個子編集器生成一個超編集器。n * 內容協商的值確定由哪一個子編集器具體完成轉換。n */n def oneOf[A, B](marshallers: Marshaller[A, B]*): Marshaller[A, B] =n Marshaller { implicit ec ? a ? FastFuture.sequence(marshallers.map(_(a))).fast.map(_.flatten.toList) }nn /**n * 輔助函數以便用多個值,以及由其中某值對應的一個子編集轉換函數組合生成一個超編集器。n * 內容協商的值確定由哪一個子編集器具體完成轉換。n */n def oneOf[T, A, B](values: T*)(f: T ? Marshaller[A, B]): Marshaller[A, B] =n oneOf(values map f: _*)nn /**n * 輔助函數用於配合相關的 ContentType 與函數生成一個同步的 [[Marshaller]] n */n def withFixedContentType[A, B](contentType: ContentType)(marshal: A ? B): Marshaller[A, B] =n strict { value ? Marshalling.WithFixedContentType(contentType, () ? marshal(value)) }nn /**n * 輔助函數用於配合相關的 Charset 與函數生成一個同步的 [[Marshaller]] n */n def withOpenCharset[A, B](mediaType: MediaType.WithOpenCharset)(marshal: (A, HttpCharset) ? B): Marshaller[A, B] =n strict { value ? Marshalling.WithOpenCharset(mediaType, charset ? marshal(value, charset)) }nn /**n * 輔助函數用於從提供的函數中對應無法協商的內容生成一個同步的 [[Marshaller]]n */n def opaque[A, B](marshal: A ? B): Marshaller[A, B] =n strict { value ? Marshalling.Opaque(() ? marshal(value)) }nn /**n * 輔助函數用於從提供的函數 A => B 以及隱式 B =>C 編集器組合生成一個 [[Marshaller]] n * 從而生成最終類型 Cn */n def combined[A, B, C](marshal: A ? B)(implicit m2: Marshaller[B, C]): Marshaller[A, C] =n Marshaller[A, C] { ec ? a ? m2.compose(marshal).apply(a)(ec) }n}n

而對應的 Unmarshaller 函數則是

**n * 用提供的函數生成一個 `Unmarshaller` n */ndef apply[A, B](f: ExecutionContext ? A ? Future[B]): Unmarshaller[A, B] =n withMaterializer(ec => _ => f(ec))nndef withMaterializer[A, B](f: ExecutionContext ? Materializer => A ? Future[B]): Unmarshaller[A, B] =n new Unmarshaller[A, B] {n def apply(a: A)(implicit ec: ExecutionContext, materializer: Materializer) =n try f(ec)(materializer)(a)n catch { case NonFatal(e) ? FastFuture.failed(e) }n }nn/**n * 輔助函數以便用提供的函數生成一個同步的 `Unmarshaller` n */ndef strict[A, B](f: A ? B): Unmarshaller[A, B] = Unmarshaller(_ => a ? FastFuture.successful(f(a)))nn/**n * 輔助函數以便用多個子反編集器生成一個超反編集器。n */ndef firstOf[A, B](unmarshallers: Unmarshaller[A, B]*): Unmarshaller[A, B] = //...n

衍生編集/反編集器

有時候開發者可以重用現有的編集器減輕工作量。思路上而言,就是通過增加某些新的邏輯去「包裝」一下現成的編集器以便「調整目標」對應開發者自己的類型。

(譯註:太啰嗦了,就是說可以利用函數式編程的函數組合特性進行複合處理而已...而不是用OOP思路繼承)

在這個角度來說把編集器包起來意味著以下兩件事的其中一種或全部:

  • 在輸入未到達已包裝的編集器前進行轉換

  • 把已包裝的編集器的輸出進行轉換

對於後者(轉換輸出),開發者可以使用 baseMarshaller.map,這裡的使用方式就用原生的 map 一樣。對於其者(轉換輸入),則有四種選擇

  • baseMarshaller.compose

  • baseMarshaller.composeWithEC
  • baseMarshaller.wrap

  • baseMarshaller.wrapWithEC

compose 的用法和 map 一樣. wrap 則是組合函數可以允許開發者改變原編集器所使用的 ContentType. 而 ...WithEC 這些則使開發者如果有需要可以使用內部掛上的 ExecutionContext ,而無需另外在當前作用域找一個。

同理反編集器則有:

  • baseUnmarshaller.transform

  • baseUnmarshaller.map
  • baseUnmarshaller.mapWithInput

  • baseUnmarshaller.flatMap

  • baseUnmarshaller.flatMapWithInput

  • baseUnmarshaller.recover

  • baseUnmarshaller.withDefaultValue

  • baseUnmarshaller.mapWithCharset (只能應用於 FromEntityUnmarshallers)
  • baseUnmarshaller.forContentTypes (只能應用於 FromEntityUnmarshallers)

(譯註,這裡的翻譯有點坑,原文的 base... 其實是指開發者在使用的原有 Marshaller,而 map 和 compose 就是如名字所提,和 Scala 的 map/compse 一樣,讀入一個類型轉換函數,生成新的 Marshaller 。Unmarshaller 同理。)

如何使用編集/反編集器

雖然在很多地方 Akka HTTP 的編集器都可被隱式使用,例如 當開發者如何用 Routing DSL complete 一個請求時。

但是,開發者同樣可以直接使用它的編集架構(特別在進行測試時)。akka.http.scaladsl.marshalling.Marshal 對象就是最好的切入點。

源碼鏈接

import scala.concurrent.Awaitnimport scala.concurrent.duration._nimport akka.http.scaladsl.marshalling.Marshalnimport akka.http.scaladsl.model._nnimport system.dispatcher // ExecutionContextnnval string = "Yeah"nval entityFuture = Marshal(string).to[MessageEntity]nval entity = Await.result(entityFuture, 1.second) //不要在非測試代碼里阻塞!nentity.contentType shouldEqual ContentTypes.`text/plain(UTF-8)`nnval errorMsg = "Easy, pal!"nval responseFuture = Marshal(420 -> errorMsg).to[HttpResponse]nval response = Await.result(responseFuture, 1.second) //不要在非測試代碼里阻塞!nresponse.status shouldEqual StatusCodes.EnhanceYourCalmnresponse.entity.contentType shouldEqual ContentTypes.`text/plain(UTF-8)`nnval request = HttpRequest(headers = List(headers.Accept(MediaTypes.`application/json`)))nval responseText = "Plaintext"nval respFuture = Marshal(responseText).toResponseFor(request) // 內容協商!na[Marshal.UnacceptableResponseContentTypeException] should be thrownBy {n Await.result(respFuture, 1.second) // 客戶端請求了 JSON, 這裡只有 text/plain!nn}n

或者 akka.http.scaladsl.unmarshalling.Unmarshal

源碼鏈接

import akka.http.scaladsl.unmarshalling.Unmarshalnimport system.dispatcher // ExecutionContextnimplicit val materializer: Materializer = ActorMaterializer()nnimport scala.concurrent.Awaitnimport scala.concurrent.duration._nnval intFuture = Unmarshal("42").to[Int]nval int = Await.result(intFuture, 1.second) //不要在非測試代碼里阻塞!nint shouldEqual 42nnval boolFuture = Unmarshal("off").to[Boolean]nval bool = Await.result(boolFuture, 1.second) //不要在非測試代碼里阻塞!nbool shouldBe falsen

推薦閱讀:

Scala從2.10開始試驗性的引入宏,是為了讓Scala獲得什麼樣的能力,這種能力在實際中究竟有什麼用處?
請教Spark 中 combinebyKey 和 reduceByKey的傳入函數參數的區別?
做scala好不好?
Dotty 開發環境搭建
又是一道入群題(Scala)

TAG:Scala | Akka | HTTP |