読者です 読者をやめる 読者になる 読者になる

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

Java Play

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

Playのデーモンを起動させずにJobを単独実行させる

Playはそれ自身がWeb/APサーバ、あるいはジョブスケジューラーとしての役割を果たすデーモンです。バッチ処理をPlayで実装するケースにおいては、Jobを実装して、アノテーションやapplication.confでcron記法でスケジューリングさせて処理を実行させることができます。
とはいえアノテーションやapplication.confでスケジューリングする手法では、ソースや設定ファイルの修正が発生しますし、常駐してるPlayに対して変更を行う場合、現在実行してるジョブどうしようとか考えだすと色々とヽ(`Д´#)ノ ムキー!!な感じになりますよね。
個人的にはデーモン化してるPlayにジョブを実行させるより、crontabでそれぞれのジョブをスケジューリングして単独実行させる方が運用的に良い気がしてます。
というわけで単独でJobを実行させるようなmainメソッドを持つExecutorを用意するのが良いかと思います。ちょっと端折って書きます(引数の処理とかエラー処理とかは今回の題材から外れるので)

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に引数渡せるような感じの実装にしてます。