はじめに
前回まではAkkaのアクターを使ってメッセージの送受信を行うアプリケーションについて紹介してきました。これらはサーバーサイドの中でもバックエンドを司る機能になります。今回は、WebシステムとしてHTTPのリクエストに対してレスポンスを返すためのエンドポイントとなるAkka HTTPを紹介します。
Akka HTTPとは
Akka HTTPはAkkaの拡張機能の一つで、その名の通りHTTPのリクエストを受け付け、処理結果などのレスポンスを返す機能を持ちます。以前紹介したPlay Frameworkは、Nettyというネットワークアプリケーションフレームワークを使っていました。一方Akka HTTPは、これまでに紹介したアクター(akka-actor)やAkka Streams(akka-stream)をベースとして独自に実装されており、Nettyのようなフレームワーク上に構築されているものではありません。また、Playもバージョン2.6からAkka HTTPを標準的に利用しており、Nettyを使っていたバージョンに比べ高速であると言われています。
Play Frameworkは画面を生成するViewの機能も備えるフルスタックなWebアプリケーションフレームワークであるのに対し、Akka HTTPは汎用的なツールキットです。REST APIを提供したいときや、クライアントサイドはモバイル向けのネイティブアプリケーションを構築し、サーバーサイドにWeb APIを作りたいときなどに便利です。
Akka HTTPを使ってみる
Akka HTTPを利用するには、他の拡張機能と同じようにビルドファイルbuild.sbtへライブラリへの依存関係を定義します。執筆時点の最新バージョン10.1.5を使用する場合は以下のように定義します。
リスト1:ビルドファイルbuild.sbtの例
1 | libraryDependencies ++= Seq( |
2 | "com.typesafe.akka" %% "akka-http" % "10.1.5" |
Akka(akka-actor)とはリリースサイクルが異なるため、バージョンも違います。
ルーティングDSL
ライブラリの設定が完了したので、実際に使ってみましょう。Akka HTTPのサーバーモジュールは、akka-http-coreとakka-httpの2つのレイヤから構成されます。akka-httpはakka-http-coreに依存するため、akka-http-coreを明示的依存関係に定義する必要はありません。akka-http-coreには低レベルの機能が、akka-httpには高レベルの機能が実装されています。細かな制御が柔軟にできる低レベルのAPIを利用することもできますが、ここではシンプルに実装できる高レベルのAPIを使用します。HTTPリクエストのルーティングは、Akka HTTPが提供するDSLを使って表現できます。
例えば、/pingのGETリクエストに対するふるまいは次のようにRouteを定義します。
リスト2:/pingへのGETリクエストに対するふるまい
05 | log.info("receive GET request /ping") |
09 | complete(HttpEntity(ContentTypes.text/html(UTF-8), "<h1>pong</h1>")) |
/pingへのGETリクエストに対して、単にpongという文字列を返しています。
GETリクエストに対して文字列を返す
サーバー起動
RouteにHTTPサーバーで待ち受けるURIを定義したので、次にHTTPサーバーを起動します。サーバーの起動はHttp().bindAndHandleメソッドの引数に、定義したRouteとバインドするホスト名・ポートを指定し呼び出します。必要に応じて、最大接続数などのサーバーへの細かな設定も指定できます。なおAkka HTTPは内部でAkka Streamsを使用するので、RunnableGraphを実行(run)するために暗黙のスコープにActorMaterializerを定義しておく必要があります。
リスト3:HTTPサーバーの起動
02 | object CafeServer extends App { |
03 | val route: Route = ??? |
05 | // RunnableGraphの実行(run)にActorMaterializerが必要 |
06 | implicit val system = ActorSystem("CafeServer", ConfigFactory.load("cafe")) |
07 | implicit val materializer = ActorMaterializer() |
10 | val bindingFuture = Http().bindAndHandle(route, "localhost", 8080) |
12 | .map { serverBinding => |
13 | log.info(s"Server online at ${serverBinding.localAddress}") |
14 | log.info("Press RETURN to stop...") |
16 | // RETURN が押されるまでサーバーを起動したままにする |
19 | serverBinding.unbind() |
24 | log.info("System terminated.") |
サーバー起動時にアドレスをログに出力し、標準入力よりEnterが送信されると停止するようにしました。では、sbt runでサーバーを起動したあと、HTTPリクエストを送信してみます。ここでは、リクエストとレスポンスの内容が分かりやすいHTTPie(リンクよりダウンロード可)というツールを使用し、HTTPリクエストを送信します。
HTTPステータス「200 OK」とともに結果「<h1>pong</h1>」が得られることでしょう。
リスト6:実行結果
4 | Content-Type: text/html; charset=UTF-8 |
5 | Date: Mon, 10 Sep 2018 08:11:55 GMT |
6 | Server: akka-http/10.1.5 |
URIを定義したRoute型は次のように定義されており、RequestContextをFuture[RouteResult]に変換する関数のエイリアスです。
リスト7:Route型の定義
1 | type Route = RequestContext => Future[RouteResult] |
先の例のようにRouteDirectivesトレイトのcompleteを使ってHTTP OKでレスポンスを返す他に、リクエストを拒否(reject)したり、例外をスローしリクエストを失敗(failWith)させたり、リダイレクト(redirect)したりできます。
リスト8:リクエストの拒否
4 | log.info("receive GET request /ping-reject") |
7 | reject(AuthorizationFailedRejection) |
リスト9:例外をスロー
4 | log.info("receive GET request /ping-fail") |
7 | failWith(new IllegalArgumentException("oops!")) |
リスト10:リクエストのリダイレクト
2 | path("ping-redirect") { |
4 | log.info("receive GET request /ping-redirect") |
7 | redirect("/ping", StatusCodes.PermanentRedirect) |
このようにディレクティブによりルートを作成できます。Akka HTTPには紹介した以外にも多くのディレクティブが用意されています(Akka公式サイト:Predefined Directives参照)。
マーシャリング・アンマーシャリング
リクエストで受信したデータを処理するため、あるいは、処理した結果をレスポンスするためにデータ形式の変換(マーシャリング・アンマーシャリング)が必要です。リクエストやレスポンスボディーのデータ形式とオブジェクト間の変換は、これらのルーティング定義とは切り離され、マーシャラー・アンマーシャラーで行われます。変換を行うためには、暗黙のスコープ内でこれらが有効となっている必要があります。そこでAkka HTTPでは、StringやByteStringをはじめ多くのマーシャラーとアンマーシャラーが予め定義されており、Marshallerオブジェクトにより提供され有効となっているため、個別にimportする必要はありません。予め定義されているマーシャラーとアンマーシャラーは、Akkaの公式サイトで確認できます(マーシャラー:Predefined Marshallers、アンマーシャラー:Predefined Unmarshallers)。また、定義されていない場合は、独自のマーシャラーを定義し、暗黙のスコープで有効にすることもできます。
JSONのマーシャリング・アンマーシャリング
例えば、JSON形式の場合はspray-jsonというライブラリがあります。これを利用してマーシャラーを定義してみましょう。
akka-http-spray-jsonへの依存関係を追加します。
リスト11:build.sbtにspray-jsonへの依存関係を追加
1 | libraryDependencies ++= Seq( |
2 | "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.5" |
SprayJsonSupportトレイトをミックスインし、マーシャラーを定義します。
リスト12:マーシャラーの定義
1 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport |
2 | import spray.json.DefaultJsonProtocol._ |
4 | object CafeServer extends App with SprayJsonSupport { |
6 | case class Order(product: String, count: Int) |
7 | implicit val orderFormat = jsonFormat2(Order) |
Order型のケースクラスを定義したあと、暗黙のスコープにOrder型とJSON形式の間をマーシャリング・アンマーシャリングするマーシャラーを定義しました。ケースクラスとJSON型のマーシャリングはjsonFormat1、jsonFormat2、……、jsonFormat22のようにケースクラスの引数の数に対応したマーシャラーが定義されています。Order型は引数が2つのため、ここではjsonFormat2を使用しました。続いて、Orderを取得するGETリクエストとOrderを登録するPUTリクエストを定義します。
/pingのGETリクエストは、ケースクラスOrderをJSON形式にマーシャリング(変換)してレスポンスします。
マーシャリング:Order型 -> JSON形式
PUTリクエストはJSON形式のリクエストをOrder型のケースクラスにアンマーシャリング(変換)して受けることができます。
アンマーシャリング:JSON -> Order型
リスト13:JSON形式のリクエストとレスポンス
05 | log.info("received GET request /order") |
09 | complete(Order("Coffee", 10)) |
13 | entity(as[Order]) { order => |
15 | log.info("received PUT request /order") |
18 | complete(s"Received order: ${order.product} * ${order.count}") |
ルートは「~」演算子で結合できます。ここではPUTとGETの2つのルートを結合しました。PUTリクエスト(Orderの登録)、GETリクエスト(Orderの参照)の順にHTTPリクエストを送信すると、次のような結果となります。
リスト14:PUTリクエストを送信した結果
4 | Content-Type: text/plain; charset=UTF-8 |
5 | Date: Mon, 10 Sep 2018 08:13:29 GMT |
6 | Server: akka-http/10.1.5 |
8 | Received order: Coffee * 10 |
PUTリクエストでOrderを送信すると、サーバー側でオーダーを登録(実装は省略)し、その結果が返ってきます。続いて、登録したOrderをGETリクエストで確認します。
リスト15:GETリクエストを送信した結果
04 | Content-Type: application/json |
05 | Date: Mon, 10 Sep 2018 08:14:05 GMT |
06 | Server: akka-http/10.1.5 |
GETリクエストを送信すると、登録されたOrder型がJSON形式でレスポンスされました。