Scala Web開發-Akka HTTP中使用JSON

Jackson

Jackson 是Java生態圈裏最流行的JSON序列化庫,它的官方網站是:https://github.com/FasterXML/jacksonhtml

爲何選擇 Jacksonjava

爲何選擇 Jackson 而不是更Scala範的 play-json、 circe、 json4s 等JSON序列化庫呢?這裏主要考慮是 Jackson 在Java生態圈裏更流行,相對熟悉的人更多,能夠必定程度上減輕Javaer們使用Scala時上手的難度。 同時,Jackson支持對大部分Java和Scala下的集合庫、數據類型的JSON序列化,而大部分Scala範的JSON庫只支持Scala的集合庫、case class和數據類型。當你的應用裏同時使用Java和Scala兩種不一樣的集合類型和Java style class與Scala case class時,Jackson均可以對齊完美支持。git

JacksonSupport

基於 Akka HTTP 的 marshal/unmarshal 機制,能夠很容易的集成各類序列化/反序列化工具。akka-http-json 這套庫就提供了9種不一樣的JSON序列化/反序列化方安供用戶選擇。github

咱們須要在 sbt 裏添加依賴:web

libraryDependencies += "de.heikoseeberger" %% "akka-http-jackson" % "1.22.0"

使用默認的 akka-http-jackson

"Default ObjectMapper" should {
  import de.heikoseeberger.akkahttpjackson.JacksonSupport._

  "從case class序列化和反序列化" in {
    val foo = Foo("bar", 2018)
    val result = Marshal(foo).to[RequestEntity].flatMap(Unmarshal(_).to[Foo]).futureValue
    foo mustBe result
  }

  "從數組case class序列化和反序列化" in {
    val foos = Seq(Foo("bar", 2018))
    val result = Marshal(foos).to[RequestEntity].flatMap(Unmarshal(_).to[Seq[Foo]]).futureValue
    foos mustBe result
  }

  "不支持OffsetDateTime" in {
    val foo = FooTime("羊八井", OffsetDateTime.now())
    val requestEntity = Marshal(foo).to[RequestEntity].futureValue
    intercept[MismatchedInputException] {
      throw Unmarshal(requestEntity).to[Foo].failed.futureValue
    }
  }
}

Full source at GitHubjson

能夠看到,默認的 akka-http-jackson 不支持 Java 8 新提供的時間/日期類型序列化,這是由於它默認使用的 Jackson ObjectMapper 沒有加載 JavaTimeModule 這個模塊在 https://github.com/FasterXML/jackson-modules-java8/tree/master/datetime 能夠找到JavaTimeModule這個模塊的更多詳細說明。api

/**
  * Automatic to and from JSON marshalling/unmarshalling usung an in-scope Jackon's ObjectMapper
  */
object JacksonSupport extends JacksonSupport {

  val defaultObjectMapper: ObjectMapper =
    new ObjectMapper().registerModule(DefaultScalaModule)
}

經過隱式值使用自定義的 ObjectMapper

首先來看看 akka-http-jackson 定義的 JacksonSupport.scala,它經過兩個隱式函數實現了 Akka HTTP 的 Marshal/Unmarshal 功能。數組

/**
    * HTTP entity => `A`
    */
  implicit def unmarshaller[A](
      implicit ct: TypeTag[A],
      objectMapper: ObjectMapper = defaultObjectMapper
  ): FromEntityUnmarshaller[A] =
    jsonStringUnmarshaller.map(
      data => objectMapper.readValue(data, typeReference[A]).asInstanceOf[A]
    )

  /**
    * `A` => HTTP entity
    */
  implicit def marshaller[Object](
      implicit objectMapper: ObjectMapper = defaultObjectMapper
  ): ToEntityMarshaller[Object] =
    Jackson.marshaller[Object](objectMapper)

能夠看到隱式函數又分別定義了兩個和一個隱式參數,而 objectMapper: ObjectMapper = defaultObjectMaper這個隱式參數定義了默認值,這樣在使用時咱們就能夠提供自定義的 ObjectMapper 來替代默認的 defaultObjectMapper。先來看看怎樣使用自定義的 ObjectMapper:app

"Custom ObjectMapper" should {
  import de.heikoseeberger.akkahttpjackson.JacksonSupport._
  implicit val objectMapper: ObjectMapper = helloscala.common.json.Jackson.defaultObjectMapper

  "支持OffsetDateTime" in {
    val foo = FooTime("羊八井", OffsetDateTime.now())
    val requestEntity = Marshal(foo).to[RequestEntity].futureValue
    val result = Unmarshal(requestEntity).to[FooTime].futureValue
    foo mustBe result
  }

  "從數組case class序列化和反序列化" in {
    val foos = Seq(FooTime("羊八井", OffsetDateTime.now()))
    val results = Marshal(foos).to[RequestEntity].flatMap(Unmarshal(_).to[Seq[FooTime]]).futureValue
    foos mustBe results
  }
}

Full source at GitHubide

經過在代碼上下文中定義一個隱式值:implicit val objectMapper: ObjectMapper = .... (變量名能夠取任何名字,不須要是objectMapper。可是須要保證在代碼上下文中只有一個ObjectMapper隱式類型。),Scala 編譯器在編譯代碼時將使用定義的隱式值傳入函數 unmarshaller 或 marshaller中以替代函數定義時設置的默認值。

自定義 ObjectMapper 定義在object Jackson.scala

implicit val defaultObjectMapper: ObjectMapper = getObjectMapper

private def getObjectMapper: ObjectMapper =
  new ObjectMapper().findAndRegisterModules
  //.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
  //.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
    .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
    .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
    .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
    .enable(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)
    .disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
    .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) // 禁止反序列化時將時區轉換爲 Z
    .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) // 容許序列化空的對象
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // 日期時間類型不序列化成時間戳
    .disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS) // 日期時間類型不序列化成時間戳
    .setSerializationInclusion(JsonInclude.Include.NON_NULL) // 序列化時不包含null的鍵

Full source at GitHub

自定義反序列化時容許的MediaType類型

默認狀況下,JacksonSupport要求客戶端提交的HTTP請求必需設置Content-Type的mime-type類型爲:application/json,但不少時候會遇到不那麼規範的客戶端,它們並未正確的設置HTTP請求頭。這時咱們能夠自定義JacksonSupport讓它在反序列化時支持其它Content-Type:這裏定義除了application/json外還支持text/plain類型的請求。

"Custom unmarshallerContentTypes" should {
  final object CustomJacksonSupport extends JacksonSupport {
    override def unmarshallerContentTypes: immutable.Seq[ContentTypeRange] =
      List(MediaTypes.`text/plain`, MediaTypes.`application/json`)
  }

  "text/plain unmarshal failed" in {
    import de.heikoseeberger.akkahttpjackson.JacksonSupport._
    val entity = HttpEntity("""{"name": "羊八井", "since": 2018}""")
    entity.contentType.mediaType mustBe MediaTypes.`text/plain`
    intercept[UnsupportedContentTypeException] {
      throw Unmarshal(entity).to[Foo].failed.futureValue
    }
  }

  "text/plain unmarshal" in {
    import CustomJacksonSupport._
    val entity = HttpEntity("""{"name": "羊八井", "since": 2018}""")
    entity.contentType.mediaType mustBe MediaTypes.`text/plain`
    val foo = Unmarshal(entity).to[Foo].futureValue
    foo mustBe Foo("羊八井", 2018)
  }
}

Full source at GitHub

在 routing DSL 裏使用

在 Akka HTTP Routing DSL 裏使用Jackson來反序列化/序列化JSON就很是簡單了。經過entity(as[FooTime])指令來將提交的JSON數據解析成 FooTime 樣本類(將調用 unmarshaller[A] 隱式函數),在complete函數響應結果時將 FooTime 對象序列化成JSON字符串並設置對應的Content-Type(調用 marshaller[A] 隱式函數)。

"routing-dsl" should {
  import akka.http.scaladsl.server.Directives._
  import de.heikoseeberger.akkahttpjackson.JacksonSupport._
  implicit val objectMapper: ObjectMapper = helloscala.common.json.Jackson.defaultObjectMapper

  val route: Route = path("api") {
    post {
      entity(as[FooTime]) { foo =>
        complete(foo.copy(since = foo.since.plusYears(1)))
      }
    }
  }

  "post json" in {
    val foo = FooTime("羊八井", OffsetDateTime.now())
    Post("/api", foo) ~> route ~> check {
      status mustBe StatusCodes.OK
      contentType.mediaType mustBe MediaTypes.`application/json`
      val payload = responseAs[FooTime]
      foo.name mustBe payload.name
      foo.since.isBefore(payload.since) mustBe true
    }
  }
}

Full source at GitHub

總結

Akka HTTP經過強大的 Marshal/Unmarshal 機制來實現數據的序列/反序列化,做爲一款工具庫 Akka HTTP 提供了足夠的靈活性,用戶能夠選擇本身喜歡的序列/反序列化工具和使用方式。對於JSON,推薦從 https://github.com/hseeberger/akka-http-json 開始,上面頗有可能找到你想要的。同時,akka-http-json也是一個不錯的學習 Akka HTTP Marshal/Unmarshal 機制的樣例。

完整的測試代碼在:data/src/test/scala/scalaweb/data/json/jackson/JacksonSupportTest.scala,能夠經過如下命令來運行它:

sbt "data/testOnly scalaweb.data.json.jackson.JacksonSupportTest"

測試結果示例: 

The source code for this page can be found here.

本文節選自《Scala Web開發》一書,完整內容見:https://www.yangbajing.me/scala-web-development/data/data.1.html

相關文章
相關標籤/搜索