Akka HTTPでWeb APIに仕立てる

2018年9月20日(木)
前出 祐吾
第9回目となる今回は、Akkaの拡張機能Akka HTTPを用いてWeb APIを作成する方法を紹介します。

はじめに

前回までは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の例

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-http"   % "10.1.5"
  )

Akka(akka-actor)とはリリースサイクルが異なるため、バージョンも違います。

ルーティングDSL

ライブラリの設定が完了したので、実際に使ってみましょう。Akka HTTPのサーバーモジュールは、akka-http-coreakka-httpの2つのレイヤから構成されます。akka-httpakka-http-coreに依存するため、akka-http-coreを明示的依存関係に定義する必要はありません。akka-http-coreには低レベルの機能が、akka-httpには高レベルの機能が実装されています。細かな制御が柔軟にできる低レベルのAPIを利用することもできますが、ここではシンプルに実装できる高レベルのAPIを使用します。HTTPリクエストのルーティングは、Akka HTTPが提供するDSLを使って表現できます。

例えば、/pingのGETリクエストに対するふるまいは次のようにRouteを定義します。

リスト2:/pingへのGETリクエストに対するふるまい

val route: Route =
  path("ping") {
    get {
      // ここにふるまいを定義する
      log.info("receive GET request /ping")
      // ...

      // HTTP OKでレスポンスを返す
      complete(HttpEntity(ContentTypes.text/html(UTF-8), "<h1>pong</h1>"))
    }
  }

/pingへのGETリクエストに対して、単にpongという文字列を返しています。

GETリクエストに対して文字列を返す

GETリクエストに対して文字列を返す

サーバー起動

RouteにHTTPサーバーで待ち受けるURIを定義したので、次にHTTPサーバーを起動します。サーバーの起動はHttp().bindAndHandleメソッドの引数に、定義したRouteとバインドするホスト名・ポートを指定し呼び出します。必要に応じて、最大接続数などのサーバーへの細かな設定も指定できます。なおAkka HTTPは内部でAkka Streamsを使用するので、RunnableGraphを実行(run)するために暗黙のスコープにActorMaterializerを定義しておく必要があります。

リスト3:HTTPサーバーの起動

// ルートの定義
object CafeServer extends App {
  val route: Route = ???

  // RunnableGraphの実行(run)にActorMaterializerが必要
  implicit val system = ActorSystem("CafeServer", ConfigFactory.load("cafe"))
  implicit val materializer = ActorMaterializer()

  // HTTPサーバーの起動
  val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
  bindingFuture
    .map { serverBinding =>
      log.info(s"Server online at ${serverBinding.localAddress}")
      log.info("Press RETURN to stop...")

      // RETURN が押されるまでサーバーを起動したままにする
      StdIn.readLine()

      serverBinding.unbind()
      serverBinding
    }
    .onComplete { _ ⇒
      system.terminate()
      log.info("System terminated.")
    }
  //...

サーバー起動時にアドレスをログに出力し、標準入力よりEnterが送信されると停止するようにしました。では、sbt runでサーバーを起動したあと、HTTPリクエストを送信してみます。ここでは、リクエストとレスポンスの内容が分かりやすいHTTPie(リンクよりダウンロード可)というツールを使用し、HTTPリクエストを送信します。

リスト4:サーバーの起動

sbt run

リスト5:HTTPリクエストの送信

http GET http://localhost:8080/ping

HTTPステータス「200 OK」とともに結果「<h1>pong</h1>」が得られることでしょう。

リスト6:実行結果

> http GET http://localhost:8080/ping
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/html; charset=UTF-8
Date: Mon, 10 Sep 2018 08:11:55 GMT
Server: akka-http/10.1.5

<h1>pong</h1>

URIを定義したRoute型は次のように定義されており、RequestContextFuture[RouteResult]に変換する関数のエイリアスです。

リスト7:Route型の定義

type Route = RequestContext => Future[RouteResult]

先の例のようにRouteDirectivesトレイトのcompleteを使ってHTTP OKでレスポンスを返す他に、リクエストを拒否(reject)したり、例外をスローしリクエストを失敗(failWith)させたり、リダイレクト(redirect)したりできます。

リスト8:リクエストの拒否

val route: Route =
  path("ping-reject") {
    // ここにふるまいを定義する
    log.info("receive GET request /ping-reject")
    // ...

    reject(AuthorizationFailedRejection)
  }

リスト9:例外をスロー

val route: Route =
  path("ping-fail") {
    // ここにふるまいを定義する
    log.info("receive GET request /ping-fail")
    // ...

    failWith(new IllegalArgumentException("oops!"))
  }

リスト10:リクエストのリダイレクト

val route: Route =
  path("ping-redirect") {
    // ここにふるまいを定義する
    log.info("receive GET request /ping-redirect")
    // ...

    redirect("/ping", StatusCodes.PermanentRedirect)
  }

このようにディレクティブによりルートを作成できます。Akka HTTPには紹介した以外にも多くのディレクティブが用意されています(Akka公式サイト:Predefined Directives参照)。

マーシャリング・アンマーシャリング

リクエストで受信したデータを処理するため、あるいは、処理した結果をレスポンスするためにデータ形式の変換(マーシャリング・アンマーシャリング)が必要です。リクエストやレスポンスボディーのデータ形式とオブジェクト間の変換は、これらのルーティング定義とは切り離され、マーシャラー・アンマーシャラーで行われます。変換を行うためには、暗黙のスコープ内でこれらが有効となっている必要があります。そこでAkka HTTPでは、StringByteStringをはじめ多くのマーシャラーとアンマーシャラーが予め定義されており、Marshallerオブジェクトにより提供され有効となっているため、個別にimportする必要はありません。予め定義されているマーシャラーとアンマーシャラーは、Akkaの公式サイトで確認できます(マーシャラー:Predefined Marshallers、アンマーシャラー:Predefined Unmarshallers)。また、定義されていない場合は、独自のマーシャラーを定義し、暗黙のスコープで有効にすることもできます。

JSONのマーシャリング・アンマーシャリング

例えば、JSON形式の場合はspray-jsonというライブラリがあります。これを利用してマーシャラーを定義してみましょう。

akka-http-spray-jsonへの依存関係を追加します。

リスト11:build.sbtにspray-jsonへの依存関係を追加

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.5"
  )

SprayJsonSupportトレイトをミックスインし、マーシャラーを定義します。

リスト12:マーシャラーの定義

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import spray.json.DefaultJsonProtocol._

object CafeServer extends App with SprayJsonSupport {

  case class Order(product: String, count: Int)
  implicit val orderFormat = jsonFormat2(Order)
  // ...

Order型のケースクラスを定義したあと、暗黙のスコープにOrder型とJSON形式の間をマーシャリング・アンマーシャリングするマーシャラーを定義しました。ケースクラスとJSON型のマーシャリングはjsonFormat1jsonFormat2、……、jsonFormat22のようにケースクラスの引数の数に対応したマーシャラーが定義されています。Order型は引数が2つのため、ここではjsonFormat2を使用しました。続いて、Orderを取得するGETリクエストとOrderを登録するPUTリクエストを定義します。

/pingのGETリクエストは、ケースクラスOrderをJSON形式にマーシャリング(変換)してレスポンスします。

マーシャリング:Order型 -&gt; JSON形式

マーシャリング:Order型 -> JSON形式

PUTリクエストはJSON形式のリクエストをOrder型のケースクラスにアンマーシャリング(変換)して受けることができます。

アンマーシャリング:JSON -&gt; Order型

アンマーシャリング:JSON -> Order型

リスト13:JSON形式のリクエストとレスポンス

val route: Route =
  path("order") {
    get {
      // ここにふるまいを定義する
      log.info("received GET request /order")
      // ...

      // Order型を返す
      complete(Order("Coffee", 10))
    } ~
    put {
      // Order型で受け取る
      entity(as[Order]) { order =>
        // ここにふるまいを定義する
        log.info("received PUT request /order")
        // ...

        complete(s"Received order: ${order.product} * ${order.count}")
      }
    }
  }

ルートは「~」演算子で結合できます。ここではPUTとGETの2つのルートを結合しました。PUTリクエスト(Orderの登録)、GETリクエスト(Orderの参照)の順にHTTPリクエストを送信すると、次のような結果となります。

リスト14:PUTリクエストを送信した結果

> http PUT http://localhost:8080/order product=Coffee, count:=10
HTTP/1.1 200 OK
Content-Length: 27
Content-Type: text/plain; charset=UTF-8
Date: Mon, 10 Sep 2018 08:13:29 GMT
Server: akka-http/10.1.5

Received order: Coffee * 10

PUTリクエストでOrderを送信すると、サーバー側でオーダーを登録(実装は省略)し、その結果が返ってきます。続いて、登録したOrderをGETリクエストで確認します。

リスト15:GETリクエストを送信した結果

> http GET http://localhost:8080/order
HTTP/1.1 200 OK
Content-Length: 31
Content-Type: application/json
Date: Mon, 10 Sep 2018 08:14:05 GMT
Server: akka-http/10.1.5

{
    "count": 10,
    "product": "Coffee"
}

GETリクエストを送信すると、登録されたOrder型がJSON形式でレスポンスされました。

TIS株式会社

生産技術R&D室所属。これまで社内向けWebアプリケーションフレームワークの開発などシステム開発の効率化に取り組んできた。現在はリアクティブシステムをエンタープライズの分野に適用するための技術検証を行う傍ら、Lightbendの認定コンサルティングパートナーとして、同技術を用いたPoCや開発の支援などに従事している。

リアクティブシステムが日本国内で広く活用されることでエンジニアが障害対応から解放されることを願い、執筆/講演活動、Akkaのドキュメント翻訳などにも取り組んでいる。

連載バックナンバー

設計/手法/テスト技術解説
第9回

Akka HTTPでWeb APIに仕立てる

2018/9/20
第9回目となる今回は、Akkaの拡張機能Akka HTTPを用いてWeb APIを作成する方法を紹介します。
開発言語
第8回

Akka Streamsで実装するリアクティブストリーム(その2)

2018/3/13
前回に引き続き、Akka Streamsを使って実装するリアクティブストリームについて解説する。
開発言語技術解説
第7回

Akka Streamsで実装するリアクティブストリーム

2018/3/7
今回と次回の2回に分けて、Akka Streamsを使って実装するリアクティブストリームについて解説する。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています