Play Frameworkを使ったWebアプリケーション作成入門1日目

Scala研修の2週目は、Scalaの代表的なWebアプリケーションであるPlay Framework(以下Play)を使ってWebアプリケーションの作成について学びます。 Webアプリケーションは、Webサイト構築だけでなく、JSONを使ったシステム間のAPIとしても広く用いられており、実践的なScalaプログラミングを学ぶ上でとても良い題材です。 1週目で学んだことを復習しつつ、Webアプリケーションを作成する上で必要な技術を学んでいきましょう。

Play Frameworkについて

Play Frameworkは、Scala用のWebアプリケーションフレームワークで、 Scalaを提供しているLightbend社(旧Typesafe社)から同じく提供されています。

Playの説明をする前に、そもそもなぜWebアプリケーションフレームワークを使うのかについて説明します。それは、世の中のWebアプリケーションが実装しなくてはいけない要件というのはある程度似ているからです。

例を挙げて言うと、

  • URLのパスからリクエストのコントローラーへの引き換えを行うルーティング機能
  • プログラム言語で扱っている情報をHTMLに引き渡すテンプレート機能
  • データベースとの接続を行う機能
  • データベースの定義情報の更新(マイグレーション)を行う機能
  • ブラウザテスト、機能(結合)テスト、ユニットテストなどを行う機能
  • アプリケーションログを出力する機能
  • 認証、認可、CSRF対策などセキュリティに関する機能

以上のほかにも沢山の、Webアプリケーションを開発する上で実装しなくてはならない機能があります。しかしこれらの機能のほとんどが、アプリケーションに共通した実装となります。

Webアプリケーションフレームワークはこれらの共通的な機能を提供、または、組込みやすくしてくれます。 Playはその中でもフルスタックフレームワークといい、非常に多くの機能を利用することができます。

Playの公式のドキュメントScalaの項目 にはこの研修で使う機能が一通り説明されています。より詳しいことが学びたい場合はこちらを読むとよいでしょう。

最初のWebアプリケーション

では、最初の簡単な例として、クエリーパラメーターで名前を受け取り、「Hello, ○○!」と返すような簡単なWebアプリケーションを作成してみます。

サンプルコードは http://github.o-in.dwango.co.jp/Scala/textbook/tree/master/src/example_projects/web-app-1st-day-hello-world にあります。

sbtのテンプレート機能

Playは、sbtのテンプレート機能を使ってアプリケーションの雛形を作成することができます。ターミナルで以下のようなコマンドを実行すると、いくつかの質問の後でサンプルアプリケーションが作成されます。

$ sbt new playframework/play-scala-seed.g8

この雛形を見るだけで、Playの勉強になるので、実行して確認してみてください。

しかし、今回の例は非常にシンプルなアプリケーションなので、雛形を使わずにスクラッチからプログラムを作っていきましょう。

トップディレクトリ作成

hello-world というディレクトリを作成し、その中にアプリケーションを作っていくことにします。

$ mkdir hello-world
$ cd hello-world

Playのsbtプラグインを読み込めるようにする

Playはsbtプラグインとして提供されているので、最初にsbtプラグインの設定をします。

まず、sbtのプラグイン設定に必要な project ディレクトリを作成します。

$ mkdir project

そして project の下に次のような内容の plugins.sbt というファイルを作成します。

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.11")

project 以下にある .sbt.scala はトップディレクトリの build.sbt に対する設定のような役割があります。ここに addSbtPlugin の記述をすることでトップディレクトリの build.sbt でPlayのsbtプラグインが使えるようになります。

build.sbtのプロジェクトの設定

次にいつも通り build.sbt にプロジェクトの設定を作成します。

lazy val root = (project in file(".")).enablePlugins(PlayScala)

scalaVersion := "2.12.4"

libraryDependencies += guice

先ほどのsbtプラグインの設定により PlayScala というsbtプラグインが読み込めるようになっているので enablePlugins というメソッドを使って読み込みます。これだけで、Playの標準ライブラリが使えるようになります。また、プロジェクトのディレクトリ構成がPlayに合わせたものに変更されます。

たとえば、今までScalaのソースコードは src/main/scala に置いていましたが、Playでは app 以下になります。またテストコードは src/test/scala に置いていましたが、Playでは test 以下になります。その他のディレクトリに、Playの設定ファイルを置く conf やWebの静的なファイルを置く public などがあります。

さらに libraryDependencies += guice でDIコンテナのGuiceを使えるようにします。

Playのアプリケーションに最低限必要な設定ファイル

Playには起動するために必要な設定ファイルがいくつかあります。設定ファイルは conf ディレクトリに以下に作成するので、まずは conf を作成します。

$ mkdir conf

Playを起動するには、最低でも application.confroutes という2つのファイルを作成する必要があります。

conf/application.conf

Playのアプリケーションの設定をHOCONという形式で記述します。今回は単純なアプリケーションなので空のファイルを作成しておきます。

$ touch conf/application.conf

conf/routes

PlayのようなWebアプリケーションフレームワークは、アクセスされたURLから実行するプログラムを決めるためのルーターという機能を有していることがあります。Playにもそのような機能があり、ルーティングの設定は conf/routes というファイルに書きます。

今回は http://localhost:9000/?name=Namae のようなURLで HelloController というクラスの get メソッドを実行するような設定を書いてみます。

GET    /    controllers.HelloController.get(name: Option[String])

左からHTTPのメソッド、URLのパス、実行されるクラスとメソッドが指定されています。 HTTPのクエリーパラメーターはメソッドの引数に渡されます。

これでPlayを実行するのに最低限の設定はできました。あとは処理本体の HelloController を作成するだけです。

Playのコントローラー

Playのコントローラーのコードは app/controllers に置かれます。今回は app/controllers/HelloController.scala を作成します。

package controllers

import javax.inject.Inject
import javax.inject.Singleton
import play.api.mvc.AbstractController
import play.api.mvc.Action
import play.api.mvc.AnyContent
import play.api.mvc.ControllerComponents
import play.api.mvc.Request

@Singleton
class HelloController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {

  def get(name: Option[String]) =
    Action { implicit request: Request[AnyContent] =>
      Ok {
        name
          .map(s => s"Hello, $s!")
          .getOrElse("""Please give a name as a query parameter named "name".""")
      }
    }
}

Playのコントローラークラスは play.api.mvc.Controller を継承しなければなりません。また、ルーターから呼び出されるメソッド(ここではget)は play.api.mvc.Action を返さなければなりません。 HTTPステータス200のレスポンスを返したい場合は Action の返り値を Ok にします。 Action のパラメーターの implicit request の部分は今回は不要ですが、このimplicitの値を使うことがしばしばあるので、定型コードとして覚えてしまったほうがいいと思います。

そして、ルーターのところで説明したようにWebアプリケーションに http://localhost:9000/?name=Namae とアクセスすると、このクラスの get メソッドが呼び出されます。 get メソッドの name パラメーターに対して、中身が入っている場合は Hello, $s! というレスポンスを返し、入っていない場合は Please give a name as a query parameter named "name". と返すようにします。これで、このアプリケーションのプログラムは完成です。

Playアプリケーションの実行

このアプリケーションを実行するには sbt を立ち上げて run というコマンドを実行します。以下のようにsbtの引数としてコマンドを与えることもできます。

$ sbt run

Webアプリケーションが起動したのを確認したらブラウザで http://localhost:9000/?name=Namae とアクセスすると「Hello, Namae!」と表示されます。また http://localhost:9000/ とアクセスすると「Please give a name as a query parameter named "name".」と表示されます。これでアプリケーションの動作確認ができました。

演習問題:足し算をするWebアプリケーション

http://localhost:9000/plus?a=1&b=2 をブラウザに入力すると3と表示されるようなWebアプリケーションを作成してみましょう。引数が足りない場合は Please give arguments of a and b. というメッセージを返すようにしてください。

Playのテスト

次はPlayのテストの書き方について学びましょう。

サンプルコードは http://github.o-in.dwango.co.jp/Scala/textbook/tree/master/src/example_projects/web-app-1st-day-hello-world-with-test にあります。

Playの公式ドキュメント https://www.playframework.com/documentation/2.5.x/ScalaTestingWithScalaTest を参考にコントローラークラスのテストをScalaTestで記述してみます。

テストライブラリの追加

まず、Play用のScalaTestライブラリを使うために build.sbt に以下の記述を追加します。

libraryDependencies ++= Seq(
  "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0" % "test"
)

build.sbt 全体では以下のようになります。

lazy val root = (project in file(".")).enablePlugins(PlayScala)

scalaVersion := "2.11.11"

libraryDependencies ++= Seq(
  "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0" % "test"
)

コントローラーのテストの追加

それでは HelloController のテストを test/controllers/HelloControllerSpec.scala に作成します。

package controllers

import org.scalatestplus.play.PlaySpec
import play.api.test.FakeRequest
import play.api.test.Helpers._

class HelloControllerSpec extends PlaySpec {

  def controller = new HelloController()

  "get" should {
    "クエリーパラメータがある場合は「Hello, namae!」というレスポンスを返す" in {
      val name = "namae"
      val result = controller.get(Some(name))(FakeRequest())
      assert(status(result) === 200)
      assert(contentAsString(result) === s"Hello, $name!")
    }

    """クエリーパラメータがない場合は「Please give a name as a query parameter named "name".」というレスポンスを返す""" in {
      val result = controller.get(None)(FakeRequest())
      assert(status(result) === 200)
      assert(contentAsString(result) === """Please give a name as a query parameter named "name".""")
    }
  }
}

テストクラスの HelloControllerSpecPlaySpec を継承するようにします。これによりScalaTestの機能が使えるようになります。

テストケースはクエリーパラメーターがある場合とない場合の2つを作成します。

クエリーパラメーターがある場合のテストは、レスポンスのステータスコードが200であることと、レスポンスの文字列が Hello, namae! になっていることを確認しています。

クエリーパラメーターがない場合のテストは、レスポンスのステータスコードが200であることと、レスポンスの文字列が Please give a name as a query parameter named "name". であることを確認しています。

コントローラーのテストは、コントローラーが返す Action にテスト用のリクエストのモックである FakeRequest を渡すことで実行します。 FakeRequest にはリクエストのヘッダーやボディ、Cookieなど色々な情報を付け加えることができますが、今回は簡単なコントローラーなので素の状態で渡しています。

これで今回のコントローラーのテストは完成です。

Playのテスト機能には他にSeleniumを使ったWebページのテストや、ルーターや設定も含めたアプリケーション全体をテストする機能もあります。

演習問題:足し算をするWebアプリケーションのテスト

前回作成した足し算をするWebアプリケーションにテストを追加してみましょう。

メッセージの国際化(internationalisation、i18n)

次にPlayのメッセージの国際化機能を使ってみましょう。

ここでは先ほど作成したHelloアプリケーションにPlay国際化の機能を使って言語の切り替え機能を追加し、 Accept-Language: ja というヘッダーを受け取ったらレスポンスのメッセージを日本語にするようにしましょう。

サンプルコードは http://github.o-in.dwango.co.jp/Scala/textbook/tree/master/src/example_projects/web-app-1st-day-hello-world-with-i18n にあります。

メッセージの設定

conf/application.conf

まず、有効な言語について宣言しておきます。

application.conf に以下の記述を追加します。

play.i18n.langs = [ "en", "ja" ]

これで Accept-Language: enAccept-Language: ja の2つが有効になりました。

言語ファイル

国際化で使いたいメッセージはあらかじめ conf/messagesconf/messages.ja などの言語ファイルに記述しておく必要があります。

conf/messages
hello = Hello, {0}!
noQuery = Please give a name as a query parameter named "name".
conf/messages.ja
hello = {0}さん、こんにちは!
noQuery = 名前をnameというクエリパラメータで与えてください

末尾の .ja は言語を表わしています。末尾がない messages は他に言語ファイルがない場合にデフォルトで参照されるメッセージです。

つまり Accept-Language: ja の場合 messages.ja が参照され、 Accept-Language: en など ja でない場合は messages が参照されることになります。

今回は hello でクエリーパラメーターが付いている場合のメッセージを取得し、 noQuery でクエリーパラメーターが付いていない場合のメッセージを取得できるようにします。

メッセージ中の {0} は後で値を挿入することができるプレースホルダーになっています。複数の値を使いたい場合は {1}{2} と番号を増やしていきます。

これで、必要な設定は終わったので、あとはコントローラーを修正するだけです。

コントローラー

国際化されたHelloアプリケーションのコントローラーは以下のようになります。

package controllers

import javax.inject.Inject
import play.api.i18n.I18nSupport
import play.api.i18n.Messages
import play.api.i18n.MessagesApi
import play.api.mvc.Action
import play.api.mvc.Controller

class HelloController @Inject()(val messagesApi: MessagesApi) extends Controller with I18nSupport {

  def get(name: Option[String]) =
    Action { implicit request =>
      Ok {
        name
          .map(s => Messages("hello", s))
          .getOrElse(Messages("noQuery"))
      }
    }
}

変更箇所について順に見ていきます。

まず、HelloControllerが I18nSupport を継承するようになりました。

class HelloController @Inject()(val messagesApi: MessagesApi) extends Controller with I18nSupport {

I18nSupport はPlayの国際化機能で、リクエストから言語を判定する機能があります。しかし、 I18nSupportMessagesApi が抽象メソッドとして定義されており、インスタンス化するには MessagesApi の実装をおこなう必要があります。

そこで必要になるのが、Playが提供している依存性注入の機能です。 1週目の トレイトの応用編:依存性の注入によるリファクタリング により依存性の注入の概念については学びましたが、Playで採用されている依存性の注入の方法は Guice というDIコンテナです。

Guiceでは @Inject というアノテーションを使うことにより、コンストラクタ引数を注入することができます。 MessagesApi はPlayから依存性の注入により受け取るようにします。

あとは前回の例で文字列を返していたところを Messages("hello", s) のように置き換えると、メッセージが国際化されます。

動作確認

それでは作成したアプリケーションの動作を確認してみましょう。アプリケーションを起動させてから、まず英語でメッセージが表示されることを確認します。

$ curl http://localhost:9000/
Please give a name as a query parameter named "name".

次に Accept-Language ヘッダーを付けたときにメッセージが日本語になることを確認します。

$ curl -H "Accept-Language: ja" http://localhost:9000/
名前をnameというクエリパラメーターで与えてください

これでアプリケーションを国際化することができました。

演習問題:足し算をするWebアプリケーションのメッセージの国際化

足し算をするWebアプリケーションのメッセージを国際化してみましょう。

ログを出力する

次はPlayでログを出力する方法を見てみましょう。

ここではHelloアプリケーションに渡ってきたクエリーパラメーターのログをファイルに保存するようにしましょう。

サンプルコードは http://github.o-in.dwango.co.jp/Scala/textbook/tree/master/src/example_projects/web-app-1st-day-hello-world-with-logger にあります。

ログの設定(logback.xml)

Playのログ機能はJavaの Logback の機能がそのまま使われます。よってログの設定はLogbackの設定ファイルの logback.xml でおこなうことになります。 Logbackの詳細について学ぶとたいへんなので、ここではデフォルトの設定に少し変更することにします。

PlayのLogbackのデフォルトの設定は、公式ドキュメントの SettingsLogger のところにあります。

<!--
  ~ Copyright (C) 2009-2016 Lightbend Inc. <https://www.lightbend.com>
  -->
<!-- The default logback configuration that Play uses in dev mode if no other configuration is provided -->
<configuration>

  <conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%coloredLevel %logger{15} - %message%n%xException{10}</pattern>
    </encoder>
  </appender>

  <logger name="play" level="INFO" />
  <logger name="application" level="DEBUG" />

  <logger name="com.gargoylesoftware.htmlunit.javascript" level="OFF" />

  <root level="WARN">
    <appender-ref ref="STDOUT" />
  </root>

</configuration>

この設定に、新しいログの設定を足していくことにします。

まずファイルに出力するために appender の設定を追加します。

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${application.home:-.}/logs/application.log</file>
    <encoder>
        <pattern>%date [%level] from %logger in %thread - %message%n%xException</pattern>
    </encoder>
  </appender>

Logbackの FileAppender を使い <file> のところでファイル名を指定し <encoder> のところでログの形式を指定しています。

これで logs/application.log にログを出力できるようになりました。

次に今回のアプリケーションで使うカスタムロガーの設定をします。 hello という名前で先ほど作った appender に出力するようにします。

  <logger name="hello" level="DEBUG">
    <appender-ref ref="FILE" />
  </logger>

設定全体は以下のようになります。

<!--
  ~ Copyright (C) 2009-2016 Lightbend Inc. <https://www.lightbend.com>
  -->
<!-- The default logback configuration that Play uses in dev mode if no other configuration is provided -->
<configuration>

  <conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%coloredLevel %logger{15} - %message%n%xException{10}</pattern>
    </encoder>
  </appender>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${application.home:-.}/logs/application.log</file>
    <encoder>
        <pattern>%date [%level] from %logger - %message%n%xException</pattern>
    </encoder>
  </appender>

  <logger name="play" level="INFO" />
  <logger name="application" level="DEBUG" />

  <logger name="hello" level="DEBUG">
    <appender-ref ref="FILE" />
  </logger>

  <logger name="com.gargoylesoftware.htmlunit.javascript" level="OFF" />

  <root level="WARN">
    <appender-ref ref="STDOUT" />
  </root>

</configuration>

これでコントローラーでログをファイルに出力する準備ができました。

コントローラー

クエリーパラメーターをログに出力するようにしたHelloアプリケーションのコントローラーは以下のようになります。

package controllers

import play.api.mvc.Action
import play.api.mvc.Controller

class HelloController extends Controller {

  val logger = play.api.Logger("hello")

  def get(name: Option[String]) =
    Action { implicit request =>
      logger.info(s"name parameter: $name")

      Ok {
        name
          .map(s => s"Hello, ${s}!")
          .getOrElse("""Please give a name as a query parameter named "name".""")
      }
    }
}

play.api.Logger("hello") で先ほど設定したカスタムロガーを指定します。そして logger.info(s"name parameter: $name") でそのロガーを使ってクエリーパラメーターを保存するようにします。

ログには errorwarninfodebug の4つのレベルがあり、左に行くほど深刻なメッセージになっています。今回はクエリーパラメーターの情報を保存するだけなので info を使いました。

動作確認

それでは実際にこのアプリケーションを動かしてみて、動作確認してみましょう。

アプリケーションを起動させてから、クエリーパラメーターがないリクエストとクエリーパラメーターがあるリクエストを送信してみます。




$ curl http://localhost:9000/
$ curl http://localhost:9000/\?name\=Namae

そして、出力された conf/application.log を確認すると以下のようにクエリーパラメーターがログに出力されていることがわかります。

2017-05-16 12:09:30,314 [INFO] from hello - name parameter: None
2017-05-16 12:09:54,856 [INFO] from hello - name parameter: Some(Namae)

これで、ログを出力するアプリケーションを作成することができました。

演習問題:足し算をするWebアプリケーションのクエリーパラメーターのログ

足し算をするWebアプリケーションのクエリパラメーターのログを保存してみましょう。

results matching ""

    No results matching ""