Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

What is spray?

What is spray

spray | Introduction » What is spray? はてなブックマーク - spray | Introduction » What is spray?

adtech × scala勉強会でsprayの事例が幾つかあったので軽く触ってみることにしました。上のWhat-is-sprayで紹介されている内容をここでも簡単に記述しておくと以下のような感じです。2014.12.23日現在、最新のVersionは1.3.2のようです。尚、本記事の実験環境はCentOS Linux release 7.0.1406 (Core)で動かしています。

  • Akka上で動く、HTTP/RESTをサポートしたクライント/サーバーサイドのライブラリ。
  • JVMのlayerを触りたい場合もレガシーなJavaの実装が必要なくscalaAPIの記述で事が足りる。
  • sprayはフレームワークというよりはライブラリ志向で作られている(哲学的に)。
  • 完全非同期、Non-Blocking。
  • ActorとFutureベース。
  • ハイパフォーマンス、軽量、テスト可能。
  • twirlというtemplate engineもある。
  • 中心となるspray-io、インメモリのキャッシュであるspray-caching、spray-io上でClient/Serverとして動くspray-can、Servlet上で動かすためのspray-servletなどのモジュールが用意されている。

Getting Start

Hello world

Getting Started with sbt — Installing sbt on Linux はてなブックマーク - Getting Started with sbt — Installing sbt on Linux
まずはscalaのbuild toolをinstall、次にsprayを動かしてみます。プロジェクト名はhello-worldとします。以下の実行によりHTMLを吐き出すところまでは出来ました。spray-templateはspray-can上でspray-routingを実行するためのtemplateなようです。

$ wget https://dl.bintray.com/sbt/rpm/sbt-0.13.7.rpm
$ sudo yum localinstall sbt-0.13.7.rpm -y
$ git clone git://github.com/spray/spray-template.git hello-world
$ cd hello-world
$ sbt
> test
> re-start
$ curl "http://127.0.0.1:8080/"

<html>
   <body>
      <h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1>
   </body>
</html> 
Source Code

以下のパスに設定ファイルおよび起動、実行されているプログラムが記載されています。

hello-world/src/main/resources/application.conf
hello-world/src/main/scala/com/example/Boot.scala
hello-world/src/main/scala/com/example/MyServer.scala

実行したいPathinfoを切り替えたい場合はMyServer.scalaの中のMyServiceに記載されているpathのパラメータを変更します。

// this trait defines our service behavior independently from the service actor
trait MyService extends HttpService {

  val myRoute =
    path("hello") {
      get {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete {
            <html>
              <body>
                <h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1>
              </body>
            </html>
          }
        }
      }
    }
}

re-start後に再度curlします。helloのパスが有効化されました。

$ curl "http://127.0.0.1:8080/"
The requested resource could not be found.
$ curl "http://127.0.0.1:8080/hello"

<html>
   <body>
      <h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1>
   </body>
</html>

以下のように記述するとpathの条件分岐が可能です。

val myRoute =
    path("hello") {
      get {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete {
            <html>
              <body>
                <h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1>
              </body>
            </html>
          }
        }
      }
    } ~
    path("world") {
      get {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete {
            <html>
              <body>
                 <h1>Say world to <i>spray-routing</i> on <i>spray-can</i>!</h1>
              </body>
            </html>
          }
        }
      }
    }

上記はvalおよびpathの定義で分岐させましたが、traitの分岐も可能です。こうするとCodeの可読性が良くなる気がします。

class MyServiceActor
  extends Actor
  with ServiceHello
  with ServiceWorld {

  def actorRefFactory = context

  def receive = runRoute(helloRoute ~ worldRoute)
}


trait ServiceHello extends HttpService {

  val helloRoute =
    path("hello") {
      get {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete {
            <html>
              <body>
                <h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1>
              </body>
            </html>
          }
        }
      }
    }
}

trait ServiceWorld extends HttpService {

  val worldRoute =
    path("world") {
      get {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete {
            <html>
              <body>
                <h1>Say world to <i>spray-routing</i> on <i>spray-can</i>!</h1>
              </body>
            </html>
          }
        }
      }
    }
}
twirl

spray/twirl はてなブックマーク - spray/twirl
TwirlというTemplateEngineを導入してみます。まずはtwirlをproject/plugin.sbt、build.sbtに以下の内容を追加します。各行の追加は空行が必要なので気をつけてください。

# project/plugin.sbt
addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2")

resolvers += "spray repo" at "http://repo.spray.io"

addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")

# build.sbt
Twirl.settings

次にtemplateを追加します。main直下にtwirlディレクトリを作成し、index.scala.htmlなどを配置します。index.scala.htmlの内容を以下のものとします。templateの記述方法はplayframeworkと同等になります。最後にMyServer.scalaで変数のpに値を渡してあげると、埋め込んで出力してくれます。

// MyServer.scala
trait MyService extends HttpService {

  val myRoute =
    path("") {
      get {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete (
            html.index.render("twirl").body
          )
        }
      }
    }
}

// index.scala.html
@(p: String)

<!DOCTYPE html>
<html>
<body>
  <p>Hello-World @p</p>
</body>
</html>
curl "http://127.0.0.1:8080/"

<!DOCTYPE html>
<html>
<body>
  <p>Hello-World twirl</p>
</body>
</html>

MysqlとのConnection

Slick OR Mapper

sprayからmysqlに接続するためにSlickというOR Mapperを利用します。まずはmysqlとの接続driverとslickをbuild.sbtに加えます。Slickは使用するVersionによって記述が大きく異なるので注意が必要です。この記事では2.1.0を利用しています。Slick - Scala Language Integrated Connection Kit — Slick 2.1.0 documentation はてなブックマーク - Slick - Scala Language Integrated Connection Kit — Slick 2.1.0 documentation

$ diff -u build.sbt build.sbt.bak
--- build.sbt	2014-12-23 13:45:33.476276736 +0900
+++ build.sbt.bak	2014-12-23 13:39:23.122278989 +0900
@@ -16,8 +16,6 @@
     "com.typesafe.akka"   %%  "akka-actor"    % akkaV,
     "com.typesafe.akka"   %%  "akka-testkit"  % akkaV   % "test",
     "org.specs2"          %%  "specs2-core"   % "2.3.11" % "test",
-    "mysql" % "mysql-connector-java" % "5.1.25",
-    "com.typesafe.slick" %% "slick" % "2.1.0"
   )
 }

あまり良く無い例ですが、http://127.0.0.1:8080にアクセスしたタイミングでMyServiceから直接Mysqlに接続してTableの作成、データのInsertを行います。理想としてはDBSchemaClass、Connectionを別途定義することだと思うので、気になった人は自由に書き変えてください。

trait MyService extends HttpService {
  class Customers(tag: Tag) extends Table[(Int, String, String)](tag, "customers") {
    def id = column[Int]("id", O.PrimaryKey)
    def name = column[String]("name")
    def email = column[String]("email")
    def * = (id, name, email)
  }
  val customers = TableQuery[Customers]
  Database.forURL("jdbc:mysql://localhost/test?user=root&password=",
    driver = "com.mysql.jdbc.Driver") withSession {
    implicit session =>
      try {
        (customers.ddl).drop
      } catch {
        case e:Exception => println("table not found")
      }
      (customers.ddl).create
      customers ++= Seq(
        (1, "foo", "foo@com"),
        (2, "bar", "bar@com")
      )
  }

  val myRoute =
    path("") {
      get {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete {
            <html>
              <body>
                Create/InsertDB is Completed
              </body>
            </html>
          }
        }
      }
    }
}
$ curl "http://127.0.0.1:8080/"
<html>
  <body>
     Create/InsertDB is Completed
  </body>
</html>

mysql> select * from customers;
+----+------+---------+
| id | name | email   |
+----+------+---------+
|  1 | foo  | foo@com |
|  2 | bar  | bar@com |
+----+------+---------+
2 rows in set (0.00 sec)