このエントリーをはてなブックマークに追加

Ratpackについて(後編)

はじめに

この記事は、G*Advent Calendar(Groovy,Grails,Gradle,Spock...) Advent Calendar 2014 の7日目として書かれたものです。

6日目は @grimrose さんの Ratpackについて(前編) です。

8日目は、@nobusue さんの Gradle Groovy Shellプラグインを使って依存ライブラリ込みのREPLを起動する #gadvent です。

Ratpackプロジェクトの作り方

Ratpackは、lazybones を使うと簡単にプロジェクトを作る事が出来ます。

lazybonesは GVM からインストールすることが出来ます。

$ gvm install lazybones

lazybonesからRatpackのプロジェクトを作る場合、無印とliteが選べますが、今回はmoduleを作りますので無印で行います。

liteの方は、クラスやテストのディレクトリが無いシンプルな構造をしています。目的にあわせてプロジェクトを作る事が出来ますので、Hello Worldを試したい場合は、liteで作ってみてはいかがでしょうか。

以下のコマンドを実行すると、その場所にプロジェクトが出来ます。

$ mkdir sample
$ cd sample
$ lazybones create ratpack .

lasybonesには、プロジェクト作成時のオプションがあるので、lazybones create だけ実行して確認してください。

IntelliJ IDEAかEclipseで取り込む前に、GradleでIDEのプロジェクトファイルを作っておきましょう。

$ gradle idea
## or
$ gradle eclipse

springloadedについて

2014年12月現在、標準でspringloadedが適用されていますが、http://forum.ratpack.io/springloaded-error-td681.html を見る限り、うまく動作しない模様なので、コメントアウトしておくのをおすすめします。

ディレクトリ構成について

Ratpackは以下の様なディレクトリの構成を取ります。

特徴的なのはratpackディレクトリです。このディレクトリの中に静的なファイルやratpack.groovyを置きます。

<proj>
  |
  +- src
      |
      +- ratpack
      |     |
      |     +- ratpack.groovy
      |     +- ratpack.properties
      |     +- public          // 静的なファイル等のassets
      |          |
      |          +- images
      |          +- lib
      |          +- scripts
      |          +- styles
      |
      +- main
      |   |
      |   +- groovy
              |
              +- // アプリケーションのクラス
      |
      +- test
          |
          +- groovy
              |
              +- // Spock のテスト

起動と停止

起動する場合は、以下のコマンドで出来ます。

$ gradle run

起動すると、デフォルトでは localhost:5050 で待ち受けているので、ブラウザでアクセスしましょう。

停止は、Ctrl + Cで出来ます。

お題

今回は、メッセージの登録と表示のWebアプリケーションを作ってみます。

View

lazybonesで作ったプロジェクトのテンプレートエンジンは、GroovyTemplateなのでそのまま使います。

GroovyTemplateは、拡張子が任意なので、HTMLをそのままテンプレートとして扱う事が出来ます。

また、The MarkupTemplateEngine を使うとGroovyを使ってより柔軟にレイアウトを記載できます。

今回は、一つのページで登録と一覧を行い、一覧は、JSON APIを呼んで表示するようにしてみたいと思います。

ルーティング(Routes)

ratpack.groovy のルーティングに関する箇所、以下の通りです。

handlers {
  get {
    render groovyTemplate("index.html", title: "Message App")
  }

  handler('messages') {
    byMethod {
      get {
        redirect '/' // index.htmlへリダイレクト
      }
      post {
        // メッセージを登録する。
      }
    }
  }

  delete('messages/:id') {
    // メッセージを削除する。
  }

  handler('api') {
    // メッセージの一覧をJSONで返す。
  }

  assets "public"
}

Moduleについて

今回用意するモジュールは、以下の通りです。

  • SQL
  • HikariCP
  • RxJava
  • Jackson
  • テンプレート
  • 今回作成するメッセージに関するモジュール

それぞれを bindings ブロックにて登録します。

bindings {
  add TemplatingModule

  add(HikariModule) { HikariConfig config ->
    config.driverClassName = 'org.postgresql.Driver'

    // Herokuとローカルをここで切り替える
    def uri = new URI(System.env.DATABASE_URL ?: "postgres://test:test@localhost/gadvent2014")

    def url = "jdbc:postgresql://${uri.host}${uri.path}"
    username = uri.userInfo.split(":")[0]
    password = uri.userInfo.split(":")[1]

    config.jdbcUrl = url
    config.username = username
    config.password = password
  }
  add new SqlModule()
  add new JacksonModule()

  // 今回作成するメッセージに関するモジュール
  add new MessageModule()

  init { MessageService messageService ->
    RxRatpack.initialize()
    messageService.init()
  }
}

アプリケーション

今回作成したのは以下のクラスです。

import groovy.transform.Canonical

@Canonical
class Message {
  Long id
  Date createAt
  String contents
}

import com.google.inject.AbstractModule
import com.google.inject.Scopes

class MessageModule extends AbstractModule {

  @Override
  protected void configure() {
    bind(MessageRepository).in(Scopes.SINGLETON)
    bind(MessageService).in(Scopes.SINGLETON)
  }

}

import com.google.inject.Inject
import groovy.sql.Sql
import groovy.util.logging.Slf4j
import rx.Observable

import java.sql.Timestamp

@Slf4j
class MessageRepository {

  @Inject
  private Sql sql

  void initialize() {
    log.info "Creating tables"
    sql.executeUpdate("DROP TABLE IF EXISTS messages")
    sql.executeUpdate("CREATE TABLE IF NOT EXISTS messages (id SERIAL PRIMARY KEY, createAt TIMESTAMP, contents TEXT)")
  }

  Observable<List<Message>> findAll() {
    Observable.from(sql.rows("SELECT id, createAt, contents FROM messages ORDER BY createAt")).map {
    new Message(id: it.id, createAt: it.createAt, contents: it.contents)
    }.toList()
  }

  def insert(Message message) {
    sql.executeInsert("INSERT INTO messages (createAt, contents) VALUES (?, ?)", new Timestamp(message.createAt.getTime()), message.contents)
  }

  def delete(long id) {
    sql.executeUpdate("DELETE FROM messages WHERE id = ?", id)
  }

}

import com.google.inject.Inject
import groovy.util.logging.Slf4j

@Slf4j
class MessageService {

  @Inject
  MessageRepository repository

  void init() {
    repository.initialize()
  }

  def all() {
    repository.findAll()
  }

  def create(String contents) {
    Message message = new Message(createAt: new Date(), contents: contents)
    repository.insert(message)
  }

  def delete(long id) {
    repository.delete(id)
  }

}

これらのクラスを ratpack.groovy から利用します。

handlers { MessageService messageService ->
  get {
    render groovyTemplate("index.html",
      title: "Message App"
    )
  }

  handler('messages') {
    byMethod {
      get {
        redirect '/'
      }
      post {
        // メッセージを登録する。
        Form form = parse(Form)
        def contents = form.get('contents', '')
        if (!contents) {
          render groovyTemplate('index.html',
            title: "Message App",
            errorMessage: 'メッセージを入力してください'
          )
        } else {
          context.blocking {
            messageService.create(contents)
          }.then {
            redirect '/'
          }
        }
      }
    }
  }

  delete('messages/:id') {
    // メッセージを削除する。
    def id = pathTokens["id"].toLong()
    context.blocking {
      messageService.delete(id)
    }.then {
      render Jackson.json('success')
    }
  }

  handler('api') {
    // メッセージの一覧をJSONで返す。
    messageService.all().subscribe { List<Message> messages ->
      render Jackson.json(messages)
    }
  }

  assets "public"
}

今回作成したアプリケーションのソースは、こちらに公開しています。

https://github.com/grimrose/gadvent2014

herokuへのデプロイ

あらかじめHerokuのアカウントを登録しておきます。

以下のコマンドでログイン出来る事を確認してください。

$ heroku login

RatpackはJava8のJVMで動かす必要があるので、system.properties には、以下の様に記載します。

java.runtime.version=1.8

Ratpackのプロジェクト名によってHerokuでの設定値が利用されるので、プロジェクト名を setting.gradle に指定します。

rootProject.name = "«project name»"

Ratpackはアプリケーションのポートを 5050 をデフォルトとして利用しますが、Herokuは指定されたポートで動かす必要がありますので、Prockfile には以下の様に記載します。

また、アプリケーション固有の設定をプロジェクト名を大文字にした «PROJECT_NAME»_OPTS として設定することが出来ます。 例えば、g-advent-2014 といったプロジェクト名であれば、G_ADVENT_2014 といった様に定義されます。

web: env "«PROJECT_NAME»_OPTS=-Dratpack.port=$PORT" build/install/«project name»/bin/«project name»

ここで作成した、system.properties, setting.gradle, Procfile をコミットしたら、準備完了です。

JavaのWebアプリケーションをHerokuで動かすには、buildpack が必要になります。

Ratpackは特殊なbuildpackではなく、Gradleのbuildpackを使用します。

以下のコマンドで、Herokuに新規アプリケーションを作成します。

$ heroku create --buildpack https://github.com/heroku/heroku-buildpack-gradle

今回のアプリケーションでは、RDBMSとしてPostgreSQLを使いますので、Herokuのアドオンを追加します。

$ heroku addons:add heroku-postgresql

以下のコマンドを実行して、Herokuにpushしましょう。

$ git push heroku master

最後に、以下のコマンドで始めることが出来ます。

$ heroku open

まとめ

Ratpackは、小さなWebアプリケーションを作るのに向いているので、モジュールを組み合わせて大きくしていくのに向いてます。 また、NettyをベースにしているためWebSocketアプリケーションが作れます。

但し、いいところばかりでは無く、日本語の情報やドキュメントも不十分な箇所もあります。

また、非同期処理について知識が無いと、テストやデバッグでハマってしまったり、ブロックしてしまうコードを書いてパフォーマンスが出ないといったこともありえます。

既存のServletベースのアプリケーションでは物足りなかったり、他のNettyベースのフレームワークとは一味違った開発をしてみたいチャレンジャーな人は、試してみてはいかがでしょうか。

このエントリーをはてなブックマークに追加