調べてみたけどぜんぜん分からなくて3日くらい悩んだのでメモ.
akka-http
まずakka-httpについて.akka-httpは見てのとおりakkaで作られたHTTPライブラリで,client業もserver業もこなせる何でも屋です.じゃあakkaは何かというと,アクターモデルによる非同期・分散処理を実現するライブラリです.akkaは,scalaとjavaで使うことができます.
ところでClient-sideは3階建て構造になっていて,Request-Level Client-Side APIとHost-Level Client-Side APIとConnection-Level Client-Side APIから構成されており,Connection-Levelが一番下で,その上にHost-Levelが,そして最も上にRequest-Levelが載っています.大抵の目的はRequest-Levelで事足り,今回もRequest-Level Client-Side APIを使うことにします.
// build.sbt val AkkaVersion = "2.6.8" val AkkaHttpVersion = "10.2.0" libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-protobuf" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
JSONライブラリ(Jackson + json4s)
さて,HTTPリクエストをしてJSONのレスポンスが来ることが分かりきっている場合は自動的にJSONをパースしたいですよね.
PerlやRubyといったLLに近い言語では,平易なハッシュとしてパースすることが多いのですが,Scalaは強い静的型付けなので適当にやるとMap[String, Any]
みたいな悲惨な状態になってしまいます.
scalaとしては,あらかじめこういうオブジェクトが返ってくるというのを期待していますから,その型に治めてほしいものです.例えば以下の例みたいに.
// Example case class case class QueueResult( name: String, polling_interval: Long, max_workers: Long ) // List[QueueResult] が返ってきてほしい!!!!!
幸運なことに,そのへんをうまくやってくれるJSONライブラリがscalaには数多くあります.今回はJacksonを使うことにします.
// build.sbt libraryDependencies += "com.fasterxml.jackson.core" % "jackson-core" % "2.10.5"
とはいえJSONライブラリが乱立するとインターフェイスが滅茶苦茶になってしまいます.これでは使いにくいですね.世の中うまくできているもので,JSONライブラリを統一したインターフェイスで扱うためのjson4sというライブラリがあります(こうしてどんどんライブラリが増えていくんですね).今回もこれを使います.
// build.sbt libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.6.9"
ちなみに,上掲のcase classはFireworqというジョブキューのとあるエンドポイントのレスポンスから持ってきています.なぜかというと,Fireworqを使いたかったからです.
akka-http-json
さて,Jacksonとjson4sを導入すればJSONをパースできるようになるのですが,このままではHTTPリクエストをcase classに変換するまでに以下の手順を踏む必要があります.
- akka-httpでリクエストを送信する
- 返ってきたレスポンスのbodyをStringに変換する
- json4sを使ってStringを目標のcase classに変換する
これを毎回やるのは大変です.
そこで,akka-httpとjsonライブラリとをスムーズに繋ぐライブラリが登場しました.それがakka-http-jsonです.このライブラリはjson4sにも対応しており,akka-httpが持ち帰ったレスポンスを自動的にcase classに変換してくれます.ちなみに,このパースと型変換のことをakkaまわりではunmarshallと呼んでいるようです.
// build.sbt libraryDependencies += "de.heikoseeberger" %% "akka-http-json4s" % "1.31.0"
実践
とまあここまで言うのは簡単なのですが,巷にまったくサンプルが不足しているので,実際にこれらを合体させるとさっぱりうまくいかず困ってしまいます.こういうライブラリはimplicit conversionで全部なんとかしているようなもんなので,いい具合にimportしてやらなければ動作しませんし,implicit解決パスにうまくオブジェクトが乗らなければunmarshallするための型が通らず失敗します. 実際,うまくいくまで何度もライブラリの実装を見たりcase classの位置を動かしたりしました.
で,たどりついたサンプルがこちらです.
AkkaHttpClient.scala
こちらはakka-httpでgetするためのクライアントです.参考文献のサンプルコードを多分に使っています.
ここではレスポンスコードのパースをしていますが,bodyには手を加えずそのままResponseEntity
という型で返しています.
なんでActorSystem
が??と思われるかと思います.これは内部的にActorでHTTPまわりを扱っているようなのですが,難解で自分でも良くわかっていないのです.akka-httpはフレームワークを目指さず汎用性を重視するという哲学で設計されており,ストリーミングといった機能も提供しているので,どうしても難解になってしまうんですね.
package akkahttptest import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors import akka.http.scaladsl.client.RequestBuilding.{Get, Post, Put, Delete} import akka.http.scaladsl.model.{HttpResponse, StatusCodes, ResponseEntity} import akka.stream.ActorMaterializer case class CustomResponse[T](statusCode: Int, status: String, body: T) object AkkaHTTPClient { implicit val system = ActorSystem(Behaviors.empty, "SingleRequest") implicit val materializer = ActorMaterializer()(system.classicSystem) def get(uri: String) = { import akka.http.scaladsl.Http implicit val executionContext = system.executionContext Http().singleRequest(Get(uri)).map(extractResponse) } def extractResponse( res: HttpResponse ): CustomResponse[ResponseEntity] = { import scala.language.postfixOps val statusCode = res.status.intValue() val httpStatus = statusCode match { case StatusCodes.OK.intValue => "OK" case StatusCodes.Created.intValue => "CREATED" case StatusCodes.Unauthorized.intValue => "UNAUTHORIZED" case StatusCodes.BadRequest.intValue => "BAD_REQUEST" case StatusCodes.NotFound.intValue => "NOT_FOUND" case StatusCodes.NoContent.intValue => "NO_CONTENT" case StatusCodes.InternalServerError.intValue => "INTERNAL_SERVER_ERROR" case _ => "OTHER_STATUS_CODE" } return CustomResponse( statusCode, httpStatus, res.entity ) } }
App.scala
(クライアント)
こちらが上掲のAkkaHttpClient
を呼び出すコードで,エントリポイントです.
package akkahttptest trait JsonSupport extends de.heikoseeberger.akkahttpjson4s.Json4sSupport { // Trait Json4sSupport provides implicit marshaller / unmarshaller from Serialization and Formats. implicit val serialization = org.json4s.jackson.Serialization implicit val formats = org.json4s.jackson.Serialization.formats(org.json4s.NoTypeHints) // or: implicit val json4sFormats = org.json4s.DefaultFormats // Provides ExecutionContext for unmarshalling implicit val materializer = AkkaHTTPClient.materializer } // Example case class case class QueueResult( name: String, polling_interval: Long, max_workers: Long ) object Main extends App with JsonSupport { import scala.concurrent.duration._ import scala.concurrent.Await import scala.language.postfixOps implicit val executionContext = scala.concurrent.ExecutionContext.global import akka.http.scaladsl.unmarshalling.Unmarshal val mappedResult = for { result <- AkkaHTTPClient.get("http://localhost:8100/queues") parsedBody <- Unmarshal(result.body).to[List[QueueResult]] } yield result.copy(body = parsedBody) println(Await.result(mappedResult, 10 seconds)) }
一番のキモはUnmarshal(result.body).to[List[QueueResult]]
としている箇所ですね.Unmarshal.to
はレスポンスを要求された型へと変換しようとしますが,そのためにいくつかのimplicit valueを要求します.そのうち最も重要なのがUnmarshaller
です.JSONからcase classへの変換の実態は一手にこのインスタンスに担わされていて,良い感じに型が適合するUnmarshaller
があれば万事型変換ができる,という感じです.つまり,インスタンスさえあればJSONでなくてもよいということです.MessagePackを扱えるUnmarshaller
のインスタンスが提供されて型が適合すれば,akka-httpはMessagePackをパースできるというわけです.
で,そのインスタンスは誰が提供するのかというとakka-http-jsonです.akka-http-jsonはそれ自体ではJSONをパースする能力を持たないため,インスタンスをimplicit conversionとして提供します.
どういうことかというと,json4sが持っているパース用の値やメソッドを,いい感じにUnmarshaller
に変換してしまうことによって,akka-httpへとインスタンスを提供するのです.(着いてこられますか?)
trait JsonSupport
でいくつかのimplicit valueを置いているのはまさにこのための処理です!trait JsonSupport
はde.heikoseeberger.akkahttpjson4s.Json4sSupport
を継承しているので,先程述べたakka-http用のUnmarshaller
を提供する準備ができています.そこにorg.json4s.jackson.Serialization
とorg.json4s.jackson.Serialization.formats(org.json4s.NoTypeHints)
とをimplicit valueとして置くことで,万事全ての点が繋がり,Unmarshaller
のインスタンスが使えるようになるというわけ!
後はJsonSupport
を継承したMain
オブジェクトでAkkaHTTPClient.get()
を呼び出し,そのbodyをUnmarshal
に放り込むだけで見事型変換ができるようになります.面倒な変換定義必要無し!
めでたし!このコードを動作させるとhttp://localhost:8100/queues
にアクセスした結果がList[QueueResult]
型に変換されて画面に表示されることでしょう!
case classの場所問題
ちょっと待って!変換先のcase classの事も思い出しましょう.実験の結果,どうやらUnmarshallerからすぐ見える位置に変換先のcase class(ここではQueueResult
型ですね)が配置されていないと,Unmarshallerはunmarshalのためのインスタンスを生み出せずにコンパイルに失敗してしまうようです.本当は,こうしたunmarshal処理もAkkaHTTPClient
オブジェクトに格納してしまったほうが呼び出す側としては綺麗なのですが,うまくいかないようです.綺麗な方法があったら誰か教えて!
まとめ
こうして人類はakka-httpを使ってJSONをcase classに封じ込めて安全に外部APIを呼び出せるようになりましたとさ……