LoginSignup
18
18

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-12-23

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

このブログ記事は、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を生で使うのはさけたいところですね。

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

18
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
18