ScalaDay 23

spray-routingにPlay2ライクなコントローラを導入する

More than 3 years have passed since last update.

皆様、御機嫌よう かとうです。

このブログ記事は、Scala Advent Calendar 2014 23日目の記事です。

最近、sprayを使っているので、簡単なチップスを公開したいと思います。


sprayとは

Akka ActorベースのREST APIを実装するためのフレームワークです。Benchmarking spray によるとJVM最速の部類に入るようです。詳しくはここみてください。


spray-can

spray-canを利用するとHTTPのリクエスト/レスポンスのハンドリングをActor上で扱えるようになります。実装コード例は以下です(詳細は こちら を見てください)。HTTPのリクエストをリアクティブにハンドリングできます。

class DemoService extends Actor with ActorLogging {

// ...
def receive = {
// ...
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
sender ! HttpResponse(entity = "PONG!")
// ...


spray-routing

spray-routingは、ルーティングやアクションなどをDSLを使って宣言的に記述できる特徴を持っています。

以下はgithub上にあるサンプルです。

object Boot extends App {

implicit val system = ActorSystem("on-spray-can")

val service = system.actorOf(Props[DemoServiceActor], "demo-service")

IO(Http) ! Http.Bind(service, "localhost", port = 8080)

}

class DemoServiceActor extends Actor with DemoService {

def actorRefFactory = context

def receive = runRoute(demoRoute)

}

trait DemoService extends HttpService {

implicit def executionContext = actorRefFactory.dispatcher

val demoRoute = {
get {
pathSingleSlash {
complete(index)
} ~
path("ping") {
complete("PONG!")
} ~
path("stream1") {
// we detach in order to move the blocking code inside the simpleStringStream into a future
detach() {
respondWithMediaType(`text/html`) { // normally Strings are rendered to text/plain, we simply override here
complete(simpleStringStream)
}
}
} ~
path("stream2") {
sendStreamingResponse
} ~
path("stream-large-file") {
encodeResponse(Gzip) {
getFromFile(largeTempFile)
}
} ~
/** 以下略 */
}

/** 以下略 */

}


Play2ライクなコントローラの導入

上記のルーティング情報とアクションをつらつらならべて書くのは、さすがにつらいので、Routeを別々のtraitに記述して後で合成してもよいかもしれませんね。とはいえ、それだとtrait側にルーティング情報も一緒に記述するので、エンドポイントの数が増えると管理が大変ですねってことで、Play2ライクなコントローラが書けるようにしてみました。

やることは簡単で、Route上に処理を記述しないで、別途用意したコントローラクラスに委譲するだけです。

object Main extends App {

val userRepository = UserRepository.ofDB

val serviceActorRef = system.actorOf(Props(ServiceActor(userRepository)))

IO(Http) ! Http.Bind(serviceActorRef, interface = "127.0.0.1", port = 8080)

}

Actor内部でrunRouteに与えるRouteを定義しますが、ここではルーティング情報だけを記述します。リクエストのハンドリング自体はコントローラに委譲させて、コントローラが返すActionから得られた戻り値をレスポンスに変換します(toResponseの部分)。

case class ServiceActor(userRepository: UserRepository) extends Actor with Routes {

private val userController = UserController(userRepository)

private lazy val routes: Route = {
pathPrefix(apiVersion) {
path(users) {
implicit ctx =>
val startTimeStamp = getQueryParam("startTimeStamp").map(e => TimePoint(e.toLong))
val maxEntities = getQueryParam("maxEntities").map(_.toInt)
userController.getUsers(startTimeStamp, maxEntities)(ctx).toResponse()
}
}
}

def actorRefFactory = context

def receive = runRoute(routes)

protected def getQueryParam(key: String)(implicit ctx: RequestContext): Option[String] =
ctx.request.uri.query.get(key)

protected def parseEntity[A](implicit ctx: RequestContext, unmarshaller: Unmarshaller[A]) =
ctx.request.entity.as[A]

protected implicit def convertToResponseHolder[A](resultTry: Try[A]) = new {

def toResponse()(implicit ctx: RequestContext, marshaller: ToResponseMarshaller[A]): Unit =
resultTry.map { result =>
ctx.complete(result)
}.get
}

}

コントローラでは、Actionを返すことになります。

case class UserController(userRepository: UserRepository) {

def getUsers(startTimeStamp: Option[TimePoint], maxEntities: Option[Int]): Action[EntitiesChunk[UserId, User]] = Action { implicit ctx =>
userRepository.resolveAllBy(startTimeStamp, maxEntities).map { chunk =>
EntitiesChunk[UserId, User](chunk)
}
}

}

trait Action[R] {

def apply(request: RequestContext): Try[R]

}

object Action {

def apply[R](block: RequestContext => Try[R]) = new Action[R] {
override def apply(ctx: RequestContext): Try[R] = block(ctx)
}

}


まとめ

この方法であれば、ルーティング情報を集約でき、コントローラにリクエストのハンドリングを委譲できるようになります。

まぁ、こんなことするぐらいなら、Play2でいいじゃんというのはありますが、、、DDDを採用していてそもそもフルスタックフレームが使いづらい(ヘキサゴナルアーキテクチャを想定している)というのがあったのでSprayにしたわけですが、spray-routingを生で使うのはさけたいところですね。

ということで、参考になれば幸いです。