Using Akka HTTP (Akka HTTP を使ってみよう)
諸注意:
Akka HTTP は、独自のリリースサイクルのもとで Akka そのものとは独立したモジュールとして提供されている (らしい)。(ググる時などは注意したい)
Akka HTTP は Akka HTTP 10.2.x の間にリリースされた、Akka 2.5, 2.6 および、これ以降の 2.x のバージョンと互換性があるのだそうである。
Akka HTTP モジュールは、akka-actor や akka-stream には依存しない。
なので使用者は、互換性のある Akka のバージョンを選ぶ必要や、akka-stream のバージョンの依存関係などを自分で定義する必要があるのだそうだ。
(sbt を使っている場合は, build sbt に以下のように定義する, 詳細は後述)
val AkkaVersion = "2.6.8"
val AkkaHttpVersion = "10.2.0"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-stream" % AkkaVersion,
"com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
)
使うだけなら
Lightbend Getting Started page からパッケージバージョンを選んで、「Create a project for me!」をクリックすると、プロジェクトをダウンロードすることもできる。
また他にも、自動的に設定が済んでいる新しい Akka HTTP のプロジェクトを Giter8 template を使って、作ることもできる。
sbt new akka/akka-http-quickstart-scala.g8
他にも、テンプレートプロジェクト があるので、好みに合うものも探してみよう。
高レベルAPI を使って、サクッと Routing を設定して、HTTP サーバを立ててみよう
Akka HTTP では、ハイレベルな HTTP routing を記述するライブラリ (DSL?) を提供している。
この使い方をまずは見ていこう。
各々の「ルート」は1つ以上の「Directives」の段階から成っていて、特定のリクエストをハンドリングする。
「Directives」の詳細に関しては こちらのドキュメント に詳しい。
以下に、1つだけのパスにリクエストされた時に、マッチするルートを持つ例を示す。
これは /hello
というエンドポイントにアクセスをされた時にのみ、マッチする。
そして、マッチしたら、HTTP レスポンスの OK を意味する String
を返す。
「ルート」は Route DSL を使って、port
をバインディングすることで、HTTP リクエストを受け取れるようになる。
プロジェクトの作成
# プロジェクトのディレクトリを作成
mkdir route-sample
cd route-sample
# build.sbt を作っておく
touch build.sbt
# ソースを配置するディレクトリを作成
mkdir -p src/main/scala
build.sbt を書く
name := "route-sample"
version := "0.1"
scalaVersion := "2.13.1"
val AkkaVersion = "2.6.8"
val AkkaHTTPVersion = "10.2.0"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-stream" % AkkaVersion,
"com.typesafe.akka" %% "akka-http" % AkkaHttpVersion, // (*1)
"com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion, // (*2)
)
ここで、注意です。
introduction にあるように akka.actor.typed.ActorSystem
を import して使う場合は (*2) の "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion, // (*2)
が必要です。
"com.typesafe.akka" %% "akka-http" % AkkaHttpVersion, // (*1)
を使う場合は、import と port のバインドあたりが変わります。こっちでやる場合に関しては後述します。
ひとまず、build.sbt
には "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion, // (*2)
がある前提、Routing は introduction に従う形でしばらく進んでいきます。
Routing とサーバの受付部分を書く
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Directives.{complete, get, path}
import scala.io.StdIn
object HttpServer {
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem(Behaviors.empty, "my-server")
implicit val executionContext = system.executionContext
val route =
path("hello") {
get {
complete(HttpEntity(
ContentTypes.`text/html(UTF-8)`,
"<h1>Hello, this is my-server</h1>"
))
}
}
val host = "localhost"
val port = 8080
val bindingFuture = Http().newServerAt(host, port).bind(route)
println(s"Server online at http://localhost:8080/hello\nPress RETURN to stop...")
StdIn.readLine()
bindingFuture
.flatMap(_.unbind()) // port をアンバインド
.onComplete(_ => system.terminate()) // system を停止
}
}
実行
# current directory は route-sample です
$ sbt run
[info] Loading global plugins from $HOME/.sbt/1.0/plugins
[info] Loading project definition from .../route-sample/project
[info] Loading settings for project route-sample from build.sbt ...
[info] Set current project to route-sample (in build file:.../route-sample/)
[info] Compiling 1 Scala source to .../route-sample/target/scala-2.13/classes ...
[info] running HttpServer
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Server online at http://localhost:8080/hello
Press RETURN to stop...
と出てくるので http://localhost:8080/hello にアクセスしてみましょう。
Hello, this is my-server
と出てくると嬉しい気持ちになりますね。
サーバを停止する場合は、Enter キーを押すと
...
Server online at http://localhost:8080/hello
Press RETURN to stop...
[success] Total time: 28 s, completed 2020/08/29 13:54:52
$
となって無事停止すると思います。
とてもシンプルでしたが、ここまでで一応 Request-Responce までを体験できたと思います。
typed を使わない場合
先で、
"com.typesafe.akka" %% "akka-http" % AkkaHttpVersion, // (*1)
を使う場合は、import と port のバインドあたりが変わります。こっちでやる場合に関しては後述します。
と言いました。
akka.actor.typed.ActorSystem
ではなく、akka.actor.ActorSystem
で行う方法についても説明します。
name := "route-sample"
version := "0.1"
scalaVersion := "2.13.1"
lazy val AkkaVersion = "2.6.8"
lazy val AkkaHTTPVersion = "10.2.0"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-stream" % AkkaVersion,
"com.typesafe.akka" %% "akka-http" % AkkaHTTPVersion,
)
このとき、サーバのプログラム (src/main/scala/HttpServer.scala
) は以下のようになります。
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Directives.{complete, get, path}
import scala.io.StdIn
object HttpServer {
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem("my-server")
implicit val executionContext = system.dispatcher
val route =
path("hello") {
get {
complete(HttpEntity(
ContentTypes.`text/html(UTF-8)`,
"<h1>Hello, this is my-server</h1>"
))
}
}
val host = "localhost"
val port = 8080
val bindingFuture = Http().newServerAt(host, port).bind(route)
println(s"Server online at http://localhost:8080/hello\nPress RETURN to stop...")
StdIn.readLine()
bindingFuture
.flatMap(_.unbind()) // port をアンバインド
.onComplete(_ => system.terminate()) // system を停止
}
}
具体的に変更した箇所は、以下の2箇所です。
- ActorSystem の引数
- implicit val system = ActorSystem(Behaviors.empty, "my-server")
+ implicit val system = ActorSystem("my-server")
akka.actor.ActorSystem
では、作成するシステムの名称のみを String
型の引数で与えれば良いです。
一方、akka.actor.typed.ActorSystem
では、上記と Behaviors
(akka.actor.typed.scaladsl.Behaviors
) も合わせて渡す必要があるみたいです。
- ExecutionContext の設定
- implicit val executionContext = system.executionContext
+ implicit val executionContext = system.dispatcher
akka.actor.ActorSystem
では、ExecutionContext を system.dispatcher
で設定します。
一方、akka.actor.typed.ActorSystem
では、system.executionContext
で設定するみたいです。
ExecutionContext は、私も勉強途中なのですが、とりあえずの用途としては
bindingFuture
.flatMap(_.unbind()) // port をアンバインド
.onComplete(_ => system.terminate()) // system を停止
で使うみたいです。
最終的なプロジェクト構成
./route-sample
├── build.sbt
├── project
│ ├── build.properties
│ ├── project
│ └── target
├── src
│ └── main/scala/HttpServer.scala
└── target