みなさんいかがお過ごしでしょうか? 最近NierAutomataクリアしました。
今回はSprayからAkkaHTTPへの移行に関して紹介します。
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ですが。。
移行への道のり
- build.sbtのライブラリから全て削除
- アプリケーション起動部分の修正
- HttpServiceの削除
- JSONのMarshaller
- HTTPRequest部分
- 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とは何か?ざっくり言うと、以下です。
これは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を使ってる方は、ぜひ試してみてください。