皆様、御機嫌よう かとうです。
このブログ記事は、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を生で使うのはさけたいところですね。
ということで、参考になれば幸いです。