Akka HTTP

Akka HTTPリファレンス

Akka HTTP Scala API

scaladslのdsl: domain-specific language(DSL)

AKKA HTTPによるRSET APIの基本

JSONフォーマットによるユーザ登録・削除とユーザ、ユーザリスト参照

Akka HTTP Quickstart for Scala

Githubコード

以下4つのファイルで構成

  • QuickstartApp.scala – メインサーバアプリ
  • UserRoutes.scala – Akka HTTPによるルート定義
  • UserRegistry.scala – ユーザ登録リクエストを処理するアクター
  • JsonFormats.scala – JSONフォーマットデータへのコンバーター

以下のUserResistryアクターが、サーバのREST-APIにより、各リクエストを処理します。

ダウンロードしたサンプルコードディレクトリ内でsbtシェルを起動し、reStart(sbt-revolverプラグイン:バックグラウンド起動)により本システム起動します。

$sbt
sbt:akka-http-quickstart-scala> reStart
root Starting com.example.QuickstartApp.main()
[success] Total time: 1 s, completed 2020/11/26 18:12:28
sbt:akka-http-quickstart-scala> root [2020-11-26 18:12:30,849] [INFO] [akka.event.slf4j.Slf4jLogger] [HelloAkkaHttpServer-akka.actor.default-dispatcher-3] [] - Slf4jLogger started
root[ERROR] SLF4J: A number (1) of logging calls during the initialization phase have been intercepted and are
root[ERROR] SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
root[ERROR] SLF4J: See also http://www.slf4j.org/codes.html#replay
root [2020-11-26 18:12:32,317] [INFO] [akka.actor.typed.ActorSystem] [HelloAkkaHttpServer-akka.actor.default-dispatcher-3] [] - Server online at http://127.0.0.1:8080/

DSLにより、ユーザと各ユーザのルートは、 /users, /users/<id> に設定されます。
ユーザへのアクセスは、http://127.0.0.1:8080/users/<id>
ユーザリストは、http://127.0.0.1:8080/users
となります。

この時点ではユーザデータは空なので、実際にユーザデータを上記サーバへポストします。ポストする手段としては、cURLコマンド、またはPostmanを利用します。

cURLによるポスト

$ curl -H "Content-type: application/json" -X POST -d '{"name": "MrX", "age": 31, "countryOfResidence": "Canada"}' http://localhost:8080/users

$ curl -H "Content-type: application/json" -X POST -d '{"name": "Anonymous", "age": 55, "countryOfResidence": "Iceland"}' http://localhost:8080/users

$ curl -H "Content-type: application/json" -X POST -d '{"name": "Bill", "age": 67, "countryOfResidence": "USA"}' http://localhost:8080/users

Postmanによるポスト

Postmanダウンロード

ユーザごとに以下を参考に、JSONフォーマットによる各ユーザデータをサーバへ送信します。
postman-post

登録したユーザの確認は、例えばユーザBillの場合、

$ curl http://localhost:8080/users/Bill
{"name":"Bill","age":67,"countryOfResidence":"USA"}

全ユーザの確認は、

$ curl http://localhost:8080/users
{"users":[{"name":"Anonymous","age":55,"countryOfResidence":"Iceland"},{"name":"MrX","age":31,"countryOfResidence":"Canada"},{"name":"Bill","age":67,"countryOfResidence":"USA"}]}

Postman、ウェブブラウザでも上記データは取得できます。

ユーザを削除する場合は、

$ curl -X DELETE http://localhost:8080/users/Bill
User Bill deleted.

Directives

Akka HTTP JSON

akka-http-json provides JSON (un)marshalling support for Akka HTTP via the following JSON libraries:

akka-http-circe

https://mvnrepository.com/artifact/de.heikoseeberger/akka-http-circe

project/build.sbtへ以下追加

libraryDependencies ++= Seq(
  "de.heikoseeberger" %% "akka-http-circe" % "1.35.2",
  ...
)

Circe

val circeVersion = "0.12.3"

libraryDependencies ++= Seq(
  "io.circe" %% "circe-core",
  "io.circe" %% "circe-generic",
  "io.circe" %% "circe-parser"
).map(_ % circeVersion)

akka-http-jackson

https://mvnrepository.com/artifact/de.heikoseeberger/akka-http-jackson

project/build.sbtへ以下追加

libraryDependencies ++= Seq(
  "de.heikoseeberger" %% "akka-http-jackson" % "1.35.2",
  ...
)

Akka HTTP JSON サンプルコード

IoTの基本であるJSONフォーマットによるデータ送受信
3つのJSONライブラリ:Spray, Circe, JacksonによるAkka HTTPのサンプルコード
IntelliJにより作成)

build.sbt

name := "AkkaHttpJson"

version := "0.1"

scalaVersion := "2.13.4"

val akkaVersion = "2.6.5"
val akkaHttpVersion = "10.2.1"
val akkaHttpJsonVersion = "1.35.2"
val circeVersion = "0.12.3"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
  "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion,
  "de.heikoseeberger" %% "akka-http-circe" % akkaHttpJsonVersion,
  "de.heikoseeberger" %% "akka-http-jackson" % akkaHttpJsonVersion,
  "com.typesafe.akka" %% "akka-stream" % akkaVersion,
  "ch.qos.logback" % "logback-classic" % "1.2.3",
)

// Circe
libraryDependencies ++= Seq(
  "io.circe" %% "circe-core",
  "io.circe" %% "circe-generic",
  "io.circe" %% "circe-parser"
).map(_ % circeVersion)

AkkaHttpJson.scala

import java.util.UUID

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
import de.heikoseeberger.akkahttpjackson.JacksonSupport

/*
    We're going to work with these data structures in our endpoints.
    In practice we'd create some "request" data structures, e.g. AddUserRequest.
*/
case class Person(name: String, age: Int)
case class UserAdded(id: String, timestamp: Long)

import spray.json._

trait PersonJsonProtocol extends DefaultJsonProtocol {
  implicit val personFormat: RootJsonFormat[Person] = jsonFormat2(Person)
  implicit val userAddedFormat: RootJsonFormat[UserAdded] = jsonFormat2(UserAdded)
}

// Spray
object AkkaHttpJson extends PersonJsonProtocol with SprayJsonSupport {
  implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "AkkaHttpJson")

  val route: Route = (path("api" / "user") & post) {
    entity(as[Person]) { _: Person =>
      complete(UserAdded(UUID.randomUUID().toString, System.currentTimeMillis()))
    }
  }

  def main(args: Array[String]): Unit = {
    Http().newServerAt("localhost", 8081).bind(route)
  }
}

// Circe
object AkkaHttpCirce extends FailFastCirceSupport {
  import io.circe.generic.auto._

  implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "AkkaHttpJson")

  val route: Route = (path("api" / "user") & post) {
    entity(as[Person]) { _: Person =>
      complete(UserAdded(UUID.randomUUID().toString, System.currentTimeMillis()))
    }
  }

  def main(args: Array[String]): Unit = {
    Http().newServerAt("localhost", 8082).bind(route)
  }
}

// Jackson
object AkkaHttpJackson extends JacksonSupport {
  implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "AkkaHttpJson")

  val route: Route = (path("api" / "user") & post) {
    entity(as[Person]) { _: Person =>
      complete(UserAdded(UUID.randomUUID().toString, System.currentTimeMillis()))
    }
  }

  def main(args: Array[String]): Unit = {
    Http().newServerAt("localhost", 8083).bind(route)
  }
}

JSONライブラリを導入した各オブジェクト(サーバ)を、IntelliJのrunコマンド、またはオブジェクトの左に示されたアイコンをクリックして起動します。

JSONフォーマットのユーザデータをポスト—>JSONフォーマットのidとタイムスタンプを表示
Postmanまたはsnaphttpユーティリティにより動作を確認して下さい。

$ echo '{"name": "bill", "age": 23 }' | http post localhost:8081/api/user
HTTP/1.1 200 OK
Content-Length: 71
Content-Type: application/json
Date: Fri, 27 Nov 2020 09:06:19 GMT
Server: akka-http/10.2.1

{
    "id": "61ffca83-425c-4dfa-b7ca-5e14f0546380",
    "timestamp": 1606467979105
}

Akka HTTP 10.1.x -> 10.2.xバージョン移行時の変更点

ServerBuilder API

10.1.x

// only worked with classic actor system
implicit val system = akka.actor.ActorSystem("TheSystem")
implicit val mat: Materializer = ActorMaterializer()
val route: Route =
  get {
    complete("Hello world")
  }
Http().bindAndHandle(route, "localhost", 8080)

10.2.x

// works with both classic and typed ActorSystem
implicit val system = akka.actor.typed.ActorSystem(Behaviors.empty, "TheSystem")
// or
// implicit val system = akka.actor.ActorSystem("TheSystem")

// materializer not needed any more

val route: Route =
  get {
    complete("Hello world")
  }
Http().newServerAt("localhost", 8080).bind(route)