play framework 小ネタ1 -PlayのJobを単独実行させる-

昼の仕事ではPlay1.2系(Java)、個人的な仕事ではPlay2.0系(Scala)を使っていますが小ネタを少しずつ紹介しようかと思います。

### Playのデーモンを起動させずにJobを単独実行させる Playはそれ自身がWeb/APサーバ、あるいはジョブスケジューラーとしての役割を果たすデーモンです。バッチ処理をPlayで実装するケースにおいては、Jobを実装して、アノテーションやapplication.confでcron記法でスケジューリングさせて処理を実行させることができます。
とはいえアノテーションやapplication.confでスケジューリングする手法では、ソースや設定ファイルの修正が発生しますし、常駐してるPlayに対して変更を行う場合、現在実行してるジョブどうしようとか考えだすと色々とヽ(`Д´#)ノ ムキー!!な感じになりますよね。
個人的にはデーモン化してるPlayにジョブを実行させるより、crontabでそれぞれのジョブをスケジューリングして単独実行させる方が運用的に良い気がしてます。
というわけで単独でJobを実行させるようなmainメソッドを持つExecutorを用意するのが良いかと思います。ちょっと端折って書きます(引数の処理とかエラー処理とかは今回の題材から外れるので) ```java public class Executor { public static void main(String[] args) throws Exception { new Executor().execute(args); } public void execute(String[] args) throws Exception { String applicationPath = "/User/stormcat/workspace/job-sample"; // 実行したいJobがあるPlayプロジェクトのパス File applicationDir = new File(applicationPath); // Play初期化。モードはVM引数で渡すのが良さげ Play.init(applicationDir, System.getProperty("play.id", "dev")); if (Play.mode == Mode.DEV) { // 通常、JobはDEVモードにおいては最初のHTTPリクエストを受けた際に実行されるため、明示的にstartさせる Play.start(); } // ベタ書きだが、引数で受け取ったクラスをReflectionで取得するような感じになる String jobClass = "jobs.NekoJob"; // PlayのクラスローダーからClassを取得する。 Class<? extends Job> clazz = (Class<? extends Job>) Play.classloader.getClassIgnoreCase(jobFqdn); // Jobのインスタンス生成 Job job = clazz.newInstance(); // Jobを非同期実行する。無名Promiseを実装。 job.now().onRedeem(new Promise() { @Override public void invoke(Object result) { System.out.println("ジョブが終了しました。"); System.out.println("Playを終了します。"); Play.stop(); System.out.println("JVMを終了します。"); System.exit(0); } }); System.out.println("ジョブを実行しています。"); } } ```基本的な流れは以下のような感じ。
  1. Playプロジェクトの場所を指定して、Play#initでPlayを初期処理
  2. DEVモードの場合、Play#startで意図的にプログラムを開始させる。Playがデーモンの場合、Jobは最初のHTTPリクエストを受けてから実行されるため。
  3. 引数とかから受け取ったJobクラス文字列から、リフレクションでClass取得。**注意点はデフォルトのClassLoaderからではなく、PlayのClassLoaderから取得すること。また、getClassIgnoreCaseする前にPlay#initしてる必要がある。**
  4. Jobのインスタンスを生成し、Job#nowで実行する。無名Promiseでジョブ終了後のコールバックを実装できる。
  5. Jobが実行される。
  6. Jobが終了すると、Promise#invokeにコールバックされるので、Play#stopでPlayを停止し、System#exitでVMを終了させる。
てな感じです。実際にプロジェクトでは引数部分をちゃんと実装(args4jやcommons-cliを使うとキレイに書けますね)したり、Jobに引数渡せるような感じの実装にしてます。