KZKY memo

自分用メモ.

Scala Spray

基本

特徴

  • 完全非同期のノンブロッキング
  • Actor/Futureベース
  • ハイパフォーマンス
  • ライトウェイト
  • モジュール
  • テスト可能

モジュール

sprayはフレームワークという扱いにはしたくないようで,
sprayはライブラリのスイートと言っている.
spray-xxxのように個々のライブラリがある.

個々のライブラリとすることで,ある機能では別のライブラリを使うという選択肢を
ユーザに与えてくれている.例えば,jsonでのリスポンスにjson4sを使うとか.

HTTPのレイヤーを切り離すことで,単なるTCPサーバにすれば,高スループットを達成できる.それはakka単体(akka cluster)でもいい気がするが.

spray-routingを入れると,当然RESTfulな設計が可能.
HTTPのレイヤーを使いたいがもっと高速にしたければ,spray-canを使えば良い.

RESTful

RESTfulにすればURLスキーマの設計が美しくなるので,まずはこれを取り上げる.

RESTfulなURLスキーマを作るにはまずここをやる.

環境設定

IDE
build.gradle
...
	compile 'com.typesafe.akka:akka-actor_2.11:2.3.6'
	compile 'io.spray:spray-routing_2.11:1.3.2'
	compile 'io.spray:spray-io_2.11:1.3.2'
	compile 'io.spray:spray-can_2.11:1.3.2'
...

depencendiesにakka-2.10-2.1.xと書いてあるが,最新で問題ないとおもう.

SimpleExample

比較的簡単なサンプルらしいが.動かすまで結構大変だった...

package edu.kzk.spray.hello

import scala.concurrent.duration._
import akka.actor.ActorSystem
import spray.routing.SimpleRoutingApp
import spray.http._
import MediaTypes._
import scala.util.{ Failure, Success }

object SimpleExample extends App with SimpleRoutingApp {
  implicit val system = ActorSystem("simple-routing-app")
  import system.dispatcher

  startServer("localhost", port = 8080) {
    get {
      pathSingleSlash {
        redirect("/hello", StatusCodes.Found)
      } ~
        path("hello") {
          complete {
            <html>
              <h1>Say hello to <em>spray</em> on <em>spray-can</em>!</h1>
              <p>(<a href="/stop?method=post">stop server</a>)</p>
            </html>
          }
        }
    } ~
      (post | parameter('method ! "post")) {
        path("stop") {
          complete {
            system.scheduler.scheduleOnce(1.second)(system.shutdown())(system.dispatcher)
            "Shutting down in 1 second..."
          }
        }
      }
  }.onComplete {
    case Success(b) =>
      println(s"Successfully bound to ${b.localAddress}")
    case Failure(ex) =>
      println(ex.getMessage)
      system.shutdown()
  }
}

Longer Example

このサンプルは動かないので注意.
そのままじゃ動かないけれど,urlのパスを抜き出る例があり,
spray-routingでRESTfulな設計にできることがわかる.

import scala.concurrent.duration.Duration
import spray.routing.HttpService
import spray.routing.authentication.BasicAuth
import spray.routing.directives.CachingDirectives._
import spray.httpx.encoding._

trait LongerService extends HttpService with MyApp {

  val simpleCache = routeCache(maxCapacity = 1000, timeToIdle = Duration("30 min"))

  val route = {
    path("orders") {
      authenticate(BasicAuth(realm = "admin area")) { user =>
        get {
          cache(simpleCache) {
            encodeResponse(Deflate) {
              complete {
                // marshal custom object with in-scope marshaller
                getOrdersFromDB
              }
            }
          }
        } ~
        post {
          // decompresses the request with Gzip or Deflate when required
          decompressRequest() {
            // unmarshal with in-scope unmarshaller
            entity(as[Order]) { order =>
              // transfer to newly spawned actor
              detach() {
                complete {
                  // ... write order to DB
                  "Order received"
                }
              }
            }
          }
        }
      }
    } ~
    // extract URI path element as Int
    pathPrefix("order" / IntNumber) { orderId =>
      pathEnd {
        // method tunneling via query param
        (put | parameter('method ! "put")) {
          // form extraction from multipart or www-url-encoded forms
          formFields('email, 'total.as[Money]).as(Order) { order =>
            complete {
              // complete with serialized Future result
              (myDbActor ? Update(order)).mapTo[TransactionResult]
            }
          }
        } ~
        get {
          // JSONP support
          jsonpWithParameter("callback") {
            // use in-scope marshaller to create completer function
            produce(instanceOf[Order]) { completer => ctx =>
              processOrderRequest(orderId, completer)
            }
          }
        }
      } ~
      path("items") {
        get {
          // parameters to case class extraction
          parameters('size.as[Int], 'color ?, 'dangerous ? "no")
                  .as(OrderItem) { orderItem =>
            // ... route using case class instance created from
            // required and optional query parameters
          }
        }
      }
    } ~
    pathPrefix("documentation") {
      // cache responses to GET requests
      cache(simpleCache) {
        // optionally compresses the response with Gzip or Deflate
        // if the client accepts compressed responses
        compressResponse() {
          // serve up static content from a JAR resource
          getFromResourceDirectory("docs")
        }
      }
    } ~
    path("oldApi" / Rest) { pathRest =>
      redirect("http://oldapi.example.com/" + pathRest, StatusCodes.MovedPermanently)
    }
  }
}

SimpleRoutingAppを使わない場合

SimpleRoutingAppを使わないサンプルを書く指針.
サンプルコードはここを参照.

SimpleRoutingAppでやっていること

  • HttpServiceをextendsしたクラスを作る
  • そのクラスでActorを作る
    • 同じファイル内にあるHttpServiceActorは直接Actorをextendsしている
  • actorRefFactoryにActorのcontextをセット
  • その中のreceiveメソッドでrunRouteを呼ぶ
  • IO(Http) ? Http.Bind(serviceActor, interface, port, ... )でHttp Serverをスタート

なので,同じことをするクラスを作れば柔軟にカスタマイズできる.
Http Serverをスタートをするクラスを別に出して,ルーティングするActorを作ればいいと思う.

RoutingするActorクラス

  • HttpServiceをextendsしたtraitを作る
    • routingをspray-routingのDSLで作る
    • URLルートに応じて,別の責務を行うアクターを呼べばいいと思う
  • Actorをextendsする,上記traitをwithするクラスを作る
    • actorRefFactoryにActorのcontextをセット
    • receiveメソッドでrunRouteを呼ぶ

Http Serverを起動するクラス

  • ActorSystemを暗黙(implicit)で作る
  • RoutingするActorクラスをactorOfする
  • IO(Http) ? Http.Bind(RoutingActor, interface, port, ... )でHttp Serverをスタート

まとめると

  • RoutingするActorクラス
  • URL Routeに応じたActorクラス
  • Http Serverを起動するクラス

を作ればいいと思う.

所感

  • RestfulなHttp Serviceをすぐ作るという観点では結構面倒.
  • どのくらい高速なのかもテストしないとよくわからない.
  • DSLが多すぎて逆にシンプルでない.これこれ,無駄に多い気がする.