前回に引き継ぎ社内勉強会向けのネタです。今回はサンプルプロジェクトを雑に動かします。

筆者は趣味でしかScalaを使用したことないのでこの記事はいつも以上にアレです

今回は前回までで準備したサンプルプロジェクトを実際に動かしてみます。

環境


  • Scala 2.12.8
  • Akka HTTP 2.5.21
  • circe 0.11.1
  • Flyway 6.0.0-beta

サンプル1: シンプルなJSONを返すサンプル


sbtシェルを起動してください。起動後にrunMain net.yoshinorin.akkahttpexample.http.AkkaExampleOneと入力してください。その後にlocalhost:9000にアクセスすると下記のようなJSONが返ってくると思います。

1
{"message": "Hello Akka HTTP!!"}

このHTTPサーバのコードは下記のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object AkkaExampleOne extends App {

implicit val actorSystem: ActorSystem = ActorSystem("akkahttpexample")
implicit val materializer: ActorMaterializer = ActorMaterializer()
implicit val executionContext: ExecutionContext = actorSystem.dispatcher

val route = get {
pathEndOrSingleSlash {
complete(HttpEntity(ContentTypes.`application/json`, "{\"message\": \"Hello Akka HTTP!!\"}"))
}
}

val bindingFuture: Future[Http.ServerBinding] = Http().bindAndHandle(route, "localhost", 9000)
StdIn.readLine()
bindingFuture
.flatMap(_.unbind())
.onComplete(_ => actorSystem.terminate())

}
  • AkkaActorあたりの説明ができないので最初の3行は割愛させてください
    • 理解が怪しいですがAkka Actorを用いたストリーム処理のための準備とスレッドプール宣言だと思います
  • val route = get ...のところはルーティングですがこれはDSLで書きます
  • Enterの入力で停止するようにしています

サンプル2: データベースから値を取得して返すサンプル


次にデータベースから値を取得して返すサンプルを動かしてみます。

dockerによるデータベースの準備

今回はサンプルリポジトリに含めているdockerでデータベースを作成します。3306ポートを使用するので、localhostの3306が未使用であることを確認してください。

次にdockerディレクトリに移動して、おもむろにdocker-compose up -dしてください。これでデータベース(MariaDB)が起動しました。

データベースマイグレーション

次にテーブルの作成とテストデータの投入を行ないます。Flywayというデータベースマイグレーションライブラリを使用します。JVM言語界隈ではこれがたぶんメジャーだと思います。たぶん…[1]

sbtシェルを起動してrunMain net.yoshinorin.akkahttpexample.db.Migrationと入力してください。データベースのマイグレーションが始まります。

1
2
3
4
5
6
7
sbt:akka-http-example> runMain net.yoshinorin.akkahttpexample.db.Migration

...

23:05:51.507 [run-main-1] INFO org.flywaydb.core.internal.command.DbMigrate - Successfully applied 2 migrations to schema `akkaexample` (execution time 00:00.080s)
23:05:51.512 [run-main-1] DEBUG org.flywaydb.core.Flyway - Memory usage: 130 of 272M
[success] Total time: 1 s, completed 2019/04/01 23:05:51

終わるとflyway_shema_historyというテーブルにマイグレーションの履歴ができているのが解ります。

usersテーブルが出来上がり、テストデータが入っています。

HTTPサーバの起動

このusersテーブルのレコードを返すHTTPサーバを起動してみます。

runMain net.yoshinorin.akkahttpexample.http.AkkaExampleTwoを実行してください。起動後にlocalhost:9000/users/YoshinoriNにアクセスすると下記のような結果が返ってくると思います。

1
Users(1,YoshinoriN)

次にlocalhost:9000/users/JhonDoeにアクセスしてみましょう。すると、次のようなJSONが返ってきます。

1
{"message": "Not Found"}

このサンプルのコードは下記のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
val route = get {
pathEndOrSingleSlash {
complete(HttpEntity(ContentTypes.`application/json`, "{\"message\": \"Hello Akka HTTP!!\"}"))
} ~ pathPrefix("users") {
pathPrefix(".+".r) { userName =>
getUser(userName) match {
case Some(u) => complete(HttpEntity(ContentTypes.`application/json`, s"$u"))
case _ => complete(HttpEntity(ContentTypes.`application/json`, "{\"message\": \"Not Found\"}"))
}
}
}
}

ルーティング部のみ抜粋しました。getUserのユーザ取得処理はservices.UsersServiceに記述していますが、ここの説明は割愛します。match式で結果があればそれを返却し、なければ存在しない旨のJSONを返すようにしています。

サンプル3: データベースから値を取得してJSONを返すサンプル


前述のサンプルでは取得したユーザのデータを(いちおうapplication/jsonを指定していますが)そのまま返しています。これをちゃんとJSONにエンコードして返したいと思います。

runMain net.yoshinorin.akkahttpexample.http.AkkaExampleThreeを実行してください。起動後にlocalhost:9000/users/YoshinoriNにアクセスすると先ほどと異なりJSONで返ってきます。

1
2
3
4
{
"id" : 1,
"name" : "YoshinoriN"
}

このサンプルのコードは下記のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package net.yoshinorin.akkahttpexample.http

import scala.concurrent.{ExecutionContext, Future}
import scala.io.StdIn
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import io.circe.syntax._ // asJsonつかうのにいる
import net.yoshinorin.akkahttpexample.services.UsersService

object AkkaExampleThree extends App with UsersService {

implicit val actorSystem: ActorSystem = ActorSystem("akkahttpexample")
implicit val materializer: ActorMaterializer = ActorMaterializer()
implicit val executionContext: ExecutionContext = actorSystem.dispatcher

val route = get {
pathEndOrSingleSlash {
complete(HttpEntity(ContentTypes.`application/json`, "{\"message\": \"Hello Akka HTTP!!\"}"))
} ~ pathPrefix("users") {
pathPrefix(".+".r) { userName =>
val user = getUser(userName).asJson
complete(HttpEntity(ContentTypes.`application/json`, s"$user"))
}
}
}

val bindingFuture: Future[Http.ServerBinding] = Http().bindAndHandle(route, "localhost", 9000)
StdIn.readLine()
bindingFuture
.flatMap(_.unbind())
.onComplete(_ => actorSystem.terminate())

}

先ほどと異なりgetUserでユーザ取得後にasJsonでJSONにエンコードしています。

JSONのエンコードの定義自体はUsersServiceというTraitに記述しています。(この設計が良いかどうかは誰か教えてくれ頼む)

circeというJSONライブラリのSemi-automatic Derivationでエンコードするようにしています。下記のコードの9行目implicit val endodeUser...の箇所です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package net.yoshinorin.akkahttpexample.services

import io.circe.Encoder
import io.circe.generic.semiauto.deriveEncoder
import net.yoshinorin.akkahttpexample.models.db.{Users, UsersRepository}

trait UsersService {

implicit val encodeUser: Encoder[Users] = deriveEncoder[Users]

/**
* Get user
*
* @param userName user name
* @return
*/
def getUser(userName: String): Option[Users] = {
UsersRepository.findByName(userName) // <-ちなみにここでDBからとってきてます
}

}

この記述をすることでnet.yoshinorin.akkahttpexample.models.dbにテーブルと同じUsers の case classをエンコードできるようになります。

1
2
3
4
5
6
package net.yoshinorin.akkahttpexample.models.db

case class Users(
id: Int,
name: String
)

asJsonを使用するにはimport io.circe.syntax._が必要です。

下記はcirceのasJsonのところのコードです。asJsonEncoder[A]を暗黙のパラメータで受け取るようになっています。

1
2
3
4
5
6
7
8
9
10
package object syntax {
implicit final class EncoderOps[A](val wrappedEncodeable: A) extends AnyVal {
final def asJson(implicit encoder: Encoder[A]): Json = encoder(wrappedEncodeable)
final def asJsonObject(implicit encoder: ObjectEncoder[A]): JsonObject =
encoder.encodeObject(wrappedEncodeable)
}
implicit final class KeyOps[K](val key: K) extends AnyVal {
final def :=[A: Encoder](a: A)(implicit keyEncoder: KeyEncoder[K]): (String, Json) = (keyEncoder(key), a.asJson)
}
}

UsersService TraitでEncoder[User]の暗黙のパラメータを定義しており、サンプルのコードはそいつを継承しているのでio.circe.syntax._をimportしておけばasJsonが使えるという感じです。

おわり。

まとめ


まあ、こんな感じだと思います。正確性に欠いてると思います。あと、設計が怪しいです。すみません…


  1. 筆者はJavaどころかJVMの人間でもないので、たぶんです… ↩︎