Screaming Loud

日々是精進

SprayからAkkaHTTPへ ~ライブラリのマイグレーション~

みなさんいかがお過ごしでしょうか? 最近NierAutomataクリアしました。

今回はSprayからAkkaHTTPへの移行に関して紹介します。

f:id:yuutookun:20170331154200j:plain

SprayとAkkaHTTPとは?

  • SprayとはシンプルなScalaのHTTPフレームワーク(というよりモジュール)。すでに開発がストップしており、新しく使うのは非推奨。
  • AkkaHttpとはSprayをベースにしたHTTPライブラリでAkkaライブラリのうちの一つ。

AkkaHTTP1.0がSprayのパフォーマンスを上回ったというブログも出ています。 http://akka.io/news/2015/07/15/akka-streams-1.0-released.html

移行開始

マイグレーションガイドというのが、以下にあります。 http://doc.akka.io/docs/akka-http/10.0.5/scala/http/migration-guide/migration-from-spray.html

ここに従って進めればできるのですが、移行における指針と詰まったところを紹介できればと思います。

移行した際の感想

先に感想を述べておくと、大体のメソッドはAkkaHTTPで実装されているので、importを直せば大抵のメソッドがそのまま使えたなぁと。 ただ、APIが4本の小さいAPIですが。。

移行への道のり

  1. build.sbtのライブラリから全て削除
  2. アプリケーション起動部分の修正
  3. HttpServiceの削除
  4. JSONのMarshaller
  5. HTTPRequest部分
  6. UnMarshaller

1.build.sbt書き換え

以下を切り替え

-    "io.spray"              %%  "spray-can"         % sprayV,
-    "io.spray"              %%  "spray-routing"     % sprayV,
-    "io.spray"              %%  "spray-json"        % sprayV,
-    "io.spray"              %%  "spray-http"        % sprayV,
-    "io.spray"              %%  "spray-client"      % sprayV,
-    "io.spray"              %%  "spray-httpx"       % sprayV,
-    "io.spray"              %%  "spray-util"        % sprayV,
+    "com.typesafe.akka" %% "akka-http" % akkaHttpV,
+    "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpV,
+    "de.heikoseeberger" %% "akka-http-json4s" % "1.14.0",

2.起動部分

sprayでの記載

object Main extends App {
 implicit val system = ActorSystem()
 val listener = system.actorOf(Props(new RestAPI()), "api")
 implicit val timeout = Timeout.intToTimeout(5000)
 val config = ConfigFactory.load()

 IO(Http) ? Http.Bind(listener, interface = "localhost", port = config.getInt("http.port"))
}

AkkaHTTPでの書き換え

object Main extends App {
  implicit val system = ActorSystem("axon")
  implicit val materializer = ActorMaterializer()
  implicit val executionContext = system.dispatcher
  val config = ConfigFactory.load()

  val route = new RestAPI().routes
  Http().bindAndHandle(route, "localhost", config.getInt("http.port"))
}

3.HttpServiceの削除

今まであったHttpServiceは全て削除します。 そこに関して特に代替の何かを継承する必要はなかったです。

4.JSONのMarshaller

そもそもMarshallerとは何か?ざっくり言うと、以下です。

  • Marshaller:case classをjsonに変換
  • UnMarshaller:jsonをcase classに変換

これはjsonとかscalaに限らず、他の言語やフォーマットでも使われます。(自分は特殊なものだと思っていた。)

以下Response生成部分を作成している部分で、以下のように修正しました。

modelの処理を受け取って、ログを吐くという処理を以下のように噛ませています。

import spray.http._
import spray.httpx.marshalling._
import spray.routing._
import spray.routing.directives.OnCompleteFutureMagnet
import spray.routing.directives.RouteDirectives._
import spray.routing.directives.FutureDirectives._

def withLogAsync[A](log: String)(actionFt: => OnCompleteFutureMagnet[A])(implicit f: A => ToResponseMarshallable): Route = {
  val start = System.currentTimeMillis()

  onComplete(actionFt) {
    case Success(None) => respondWithMediaType(`application/json`) { infoLog(log, complete((200, "{}")), start) }
    case Success(p)    => infoLog(log, complete(p), start)
    case Failure(t)    => handleError()(t)
  }
}

実際に使う場合はこんな感じで作っています。

case class Debug(isDebug: Boolean)

val route = path("debug") {
  get {
    withLogAsync("debug!") {
      Future(Debug(true))
    }
  }
}

これが、akkaHTTPだと以下のように変わります。

 def withLogAsync[A](log: String)(actionFt: => Future[A])(implicit f: A => ToResponseMarshallable): Route = {
  val start = System.currentTimeMillis()

  onComplete(actionFt) {
    case Success(None) => infoLog(log, complete((200, "{}")), start)
    case Success(p)    => infoLog(log, complete(p), start)
    case Failure(t)    => handleError()(t)
  }
}

まず、respondWithMediaTypeはakkaHTTPでは移行されなかったようです。

以下のissueが議論です。
Document what to do instead, if using the (anti pattern) respondWithMediaType from Spray · Issue #190 · akka/akka-http · GitHub

なので、単純に除外します。

また、OnCompleteFutureMagnetというdirectiveがなくなっており、これは単純にFutureを定義で問題なくなっています。

infoLog部分はSprayからAkkaHTTPに変更する際には変えていません。

private def infoLog(log: String, route: Route, start: Long): Route = {
  extract { r =>
    logger.info(s"$log::${r.request.method}::${r.request.uri.path}")
  }(r => route )
}

5.HTTPRequest部分

AkkaHTTPが受けるリクエストではなく、AkkaHTTPが送信側となるRequest生成部分でも修正が必要です。 この部分の処理が分散されて実装されていると、移行が大変かと思います。

Sprayの場合、以下のようにGETと書いていました。 またHeaderに関しても、~>で繋げばよかったのですが、引数に入れるようになりました。

def header(token: String) = addHeader("X-User-Token", token)
protected def composeGetRequest(params: Seq[(String, String)], token: String): HttpRequest = {
  val query = params.map{case (k,v) => $"k=$v"}.mkString("&")
  Get(s"$url?$query") ~> header(token)
}

しかし、AkkaHTTPだと以下のようになります。 また、ActorMateriaziderというおまじないも必要です。

implicit val mat = ActorMaterializer() // これが必要に
def header(token: String) = List(RawHeader("X-User-Token", token))
protected def composeGetRequest(params: Seq[(String, String)], token: String)(implicit header: Header): HttpRequest = {
  val query = params.map{case (k,v) => $"k=$v"}.mkString("&")
  HttpRequest(HttpMethods.GET, s"$url?$query", header(token))
}

6. UnMarshaller

送る部分も変わります。

case class BaseDto[T](dataList: Seq[T])
val request: HttpRequest
val pipeline = sendReceive ~> unmarshal[BaseDto[T]]

pipeline(request)
val request: HttpRequest
val marshaller = (entityFt: Future[HttpResponse]) => entityFt.flatMap(entity => Unmarshal(entity).to[BaseDto[T]])

marshaller(http.singleRequest(request))

まとめ

移行作業を行うにあたり色々調査して深掘りできたので、SprayやAkkaHTTP周りの理解が深まりました。 まだSprayを使ってる方は、ぜひ試してみてください。