argius note

プログラミング関連

Play2.0で作ったアプリの中にバッチ的コマンドラインアプリを組み込んでみる

Play2.0で作ったWebアプリに付随するコマンドラインプログラム、いわゆるバッチ処理を、Play2.0の機能を使って実行してみようというものです。
先進的なWebアプリフレームワークの使い方としてどうなの?という点は置いといて。

  • メリット
    • 機能の再利用ができる(特にDAO)
    • 新しいプロジェクトを作らなくて済む
    • デプロイの手間が省ける
  • デメリット
    • サーバプロセスでないので、起動が遅い(常駐化すればOK?)

ただMainクラスを動かすだけなら、このようなクラスを作って、

package batch
object MyApp extends App {
  println("hello")
} 

そして、"play dist"してできたzipを展開し、展開したディレクトリで

java -cp "`dirname $0`/lib/*" batch.MyApp

とすれば実行できます。

ただし、Playの機能を使うには、これだけではダメで、

java.lang.RuntimeException: There is no started application

と怒られてしまいます。
Playが開始状態になっていない、つまり初期化されていないのです。


これは、結論から申し上げますと、適当にstartさせてしまえば良いようです。
具体的には、play.api.Playオブジェクトのstart(app)メソッドを実行すると、Playが開始状態になります。appには、適当なApplicationクラスのインスタンスをセットします。この辺は、Testモードの時に使われる(と思われる)FakeApplicationクラスなどを参考にしています。
追記(2012-11-15): テスト用のApplication生成について、APIドキュメント(リンク)に記載がありますね。
追記(2013-07-03): Play2.1では、Applicationクラスがabstractになってしまいました。代わりにDefaultApplicationクラスを使えばOKです。常にMode=Prodで良ければStaticApplicationを使ってもOK。詳細はAPIドキュメントを参照してください。

適当とは言え、モードの指定だけは個別に対応する必要があり、開発モード(Mode=Dev)かリリースモード(Mode=Prod)かを判定する必要があります。Webアプリ動作時は、おそらく、SBT経由の場合とNetty経由の場合で設定されるモードを変えるようにしているのでしょう。
今回は、システムプロパティを設定して判定する方法にしました。(詳細は後述)
Eclipseから実行する場合、"java.library.path"に"eclipse"が含まれていればDevという、アドホックな判定方法もあります。


どのモードで動作しているかを知るには、Play本体が出力するログに

[INFO] main <play> Application started (Prod)

のように出力されるのを見れば分かります。


以上を踏まえて、簡単なバッチフレームワークを作ってみました。
追記(2012-10-26): 終了時にPlay.stopを入れるのを忘れていたので追加しました。

  • BatchApplicationクラス
package batch

import play.api.Play
import play.api.Mode
import org.apache.commons.lang3.time.FastDateFormat

abstract class BatchApplication extends App {

  private def initialize = {
    val cl: ClassLoader = classOf[BatchApplication].getClassLoader
    Play.start(new play.api.Application(new java.io.File("."), cl, None, getMode))
  }

  private def getMode: Mode.Mode = {
    System.getProperty("play.mode") match {
      case "Prod" => Mode.Prod
      case _ => Mode.Dev
    }
  }

  private lazy val dateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.SSS")

  protected def msg(formatString: String, args: Any*) = {
    val s = formatString format (args: _*)
    val now = dateFormat.format(System.currentTimeMillis)
    println("%s [%s] %s" format (now, getClass.getName, s))
  }

  protected def call(name: String, f: () => Unit) {
    msg(name + " started")
    f()
    msg(name + " ended")
  }

  def setUp(): Unit = {}
  def execute(): Unit
  def cleanUp(): Unit = {}

  // main code
  msg("batch started")
  try {
    initialize
    call("setUp", setUp)
    call("execute", execute)
  } catch {
    case e => {
      msg("batch ended abnormally")
      throw e
    }
  } finally {
    try
      call("cleanUp", cleanUp)
    finally
      Play.stop
  }
  msg("batch ended normally")

}


実装クラスの例として、次のようなクラスを作ります。

package batch

object DumpEmployeeNames extends BatchApplication {

  override def execute() = {
    // Employeeモデルオブジェクト - anormを使っている
    models.Employee.findLast(10).foreach { r =>
      println("name=" + r.name)
    }
  }

}

あとは前述のとおり、

java -Dplay.mode=Prod -cp "`dirname $0`/lib/*" batch.DumpEmployeeNames

とすればOKです。



サーバ上での実行は、Jenkinsを使うと便利だと思います。