全ジョブへの共通情報の渡し方

Quartzによってジョブ(Jobインタフェースの実装オブジェクト)が実行される場合,JobオブジェクトにJobExecutionContextオブジェクトが渡される。これは,サーブレットのServletContextオブジェクトと同様に,実行対象のJobオブジェクトの状況(まさにコンテキスト)の情報が保持されている。
各ジョブのSchedulerへの登録は,Jobクラス(のサブクラス)のクラスオブジェクト(HogeJob.classなど)を登録することになるため,実際に実行対象となるJobインスタンスは,Schedulerオブジェクトへのジョブ登録時には存在しない。そのため,対象のジョブに必要な情報は,JobDataMapオブジェクトをJobDetailオブジェクトから取り出してput()メソッドにより格納することで,Jobインスタンスの実行時にJobExecutionContextオブジェクトから取得できるようになる。
例えば,

JobDetail jobDetail = new JobDetail(“hogeJob”, null, HogeJob.class);
JobDataMap jobDataMap = jobDetail.getJobDataMap();
Object param1 = …; // 渡したい情報
jobDataMap.put(“param1″, param1);
scheduler.scheduleJob(jobDetail, …);

としておくことで,

public void execute(JobExecutionContext context)
          throws JobExecutionException {
  JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
  Object param1 = jobDataMap.get(“param1″);
}

というようにJobオブジェクト呼び出し時に取得することができる。
ただし,これは自分でSchedulerオブジェクトにジョブを登録するコードを書くときの話である。JobSchedulingDataProcessorクラスを使ってXMLファイルに定義されたジョブ情報を元にスケジューリングする方式では,XMLファイルに静的に記述可能な情報,つまりリテラルしかJobDataMapオブジェクトに格納できない。
実行時にしか手に入らない情報や情報を持つオブジェクトを各Jobオブジェクトの実行直前に渡すためには,JobListenerインタフェースが使用できる。JobListenerインタフェースは,Schedulerに登録された各ジョブの実行に関連するイベントの通知を受けるためのイベントリスナーだ。
前回のTriggerListenerインタフェースと同様に,JobListenerオブジェクトの登録先は2つ存在する。

  • 各ジョブ毎
  • グローバル

グローバルとは,Schedulerオブジェクトに登録された全てのジョブに対して,一括でイベント通知を受けたい場合に便利である。つまり,1つのJobListenerオブジェクトで,全ジョブに関するイベントをハンドリング可能。例えば,全てのJobオブジェクトに対して,ある情報を共通的に渡したい場合などに使用できる。

public class JobListenerImpl implements JobListener {
  private Object param1;
  public JobListenerImpl(Object param1) {
    this.param1 = param1;
  }
  ・・・
  public void jobToBeExecuted(JobExecutionContext context) {
    JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
    jobDataMap.put(“param1″, param1);
  }

}

JobListener#jobToBeExecuted()メソッドは,ジョブの実行が確定し,さぁジョブを実行するぞ!というタイミングの直前にコールバックされるメソッドである。上記のJobListenerImplクラスでは,コンストラクタで受け取ったparam1オブジェクトを,jobToBeExecuted()メソッドに渡される処理対象のジョブのコンテキストに追加している様子がわかるだろう。これを,

Scheduler scheduler = …;
Object param1 = …;
scheduler.addGrobalJobListener(new JobListenerImpl(param1));

としてあげれば,登録された全てのジョブの実行直前に,共通的にparam1オブジェクトを渡すことができる。
ジョブをXMLファイルで記述するパターンを採用した際には,JobListenerインタフェースやTriggerListenerインタフェースを上手に使ってあげることで,ジョブを一括して処理することが可能となり,よりきめの細かいジョブスケジューリングを実現することができるようになるだろう。

Quartzでのジョブ重複起動の抑止方法

バッチ処理に欠かすことのできないジョブスケジューラ。特にJavaの場合は,JavaVMというプロセス自体が重厚なために,個々のバッチプログラムをそれぞれJavaVMプロセス起動で実行することは,バッチプログラムの本数が多くなればなるほど非現実的になる。そのために,一つのJavaVM上でのジョブスケジューリングが基本となり,つまり各バッチ処理をプロセスとしてではなく,スレッドとして実行する基盤が欲しくなってくる。
スレッドを使用したことのある開発者であれば,必要最低限なジョブスケジューリングの基盤を実装することはそう難しいことではないだろう。しかし,バッチ間の協調が求められたり,スケジューリングが複雑になったりする場合には,やはりそれなりに機能を持つ基盤をネットで探して利用したくなるだろう。
OpenSymphonyが提供しているQuartzは,そんな状況にもってこいのジョブスケジューラ向けOSSライブラリである。
Quartzの場合,スケジューリングされるジョブは,Jobインタフェースを実装したオブジェクトである。Jobインタフェースを実装したクラスを作成し,そのクラスオブジェクトをSchedulerオブジェクトに登録する。正確には,JobDetailオブジェクトにJobクラスオブジェクトを持たせて,JobDetailオブジェクトをTriggerオブジェクト(ジョブの起動タイミングを決定する)と共にSchedulerオブジェクトに登録することで,ジョブがスケジューリングされる。ジョブの起動の度に,Jobクラスオブジェクトを元にそのインスタンスが生成される。そしてJobインスタンスのexecute()メソッドが呼び出されて,バッチ処理が実行されるという仕組みである。
もちろん「毎分起動」といった設定が可能なので,繰り返しジョブを実行することが可能である。しかし,既にジョブが実行されている際に同一のジョブの重複起動は行いたくない,といったことをしたくても,QuartzのAPIからは見つけることができない。
重複起動を阻止するためには,TriggerListenerというものをうまく使うことで実現可能になる。TriggerListenerインタフェースは,ジョブの起動スケジュールを司るTriggerオブジェクトの動作の結果生じる各種イベントに対応して,何らかの処理を行いたい場合に使用するイベントリスナーだ。
TriggerListenerオブジェクトの登録は,2種類の登録先が用意されている。

  • 各Triggerオブジェクト毎
  • グローバル

グローバルとは,Schedulerに登録された全てのTriggerオブジェクトに関して,一括で扱いたい場合に使用する。つまり,1つのTriggerListenerオブジェクトで,全てのイベント通知を受けることができる。下のコードは,重複起動を阻止する処理が実装されたTriggerListenerクラスである。ちなみに,重複起動に関係のないメソッドは,省略している。

public class TriggerListenerImpl implements TriggerListener {
  private Set currentRunningJobSet;
  public TriggerListenerImpl() {
    currentRunningJobSet = new HashSet();
  }
  ・・・
  public boolean vetoJobExecution(
      Trigger trigger, JobExecutionContext context) {
    String name = context.getJobDetail().getName();
    if (currentRunningJobSet.contains(name)) {
      return true;
    } else {
      currentRunningJobSet.add(name);
      return false;
    }
  }
  public void triggerComplete(Trigger trigger,
      JobExecutionContext context, int instructionCode) {
    String name = context.getJobDetail().getName();
    currentRunningJobSet.remove(name);
  }
}

仕組み的には,起動したジョブの名前をコレクションに保持しておいて,Triggerオブジェクトによるジョブ起動の度にジョブ名がコレクション内に存在するかどうかチェック,その結果含まれていた場合は実行中と判断してジョブの実行を取りやめる,という内容である。実行中のジョブ名を保持しておくコレクションはコンストラクタに生成処理を記述する。ジョブの起動を行っていいかどうかを判断するためのvetoJobExecution()メソッドがジョブ起動の直前に呼び出されるので,その中でジョブ名の存在チェックを行い,存在していればtrueを返却してジョブの実行を抑止する。逆に存在していなければfalseを返却して,ジョブの起動を継続する。ジョブの実行完了時にはtriggerComplete()メソッドが呼び出されるので,コレクションからジョブ名を削除する。
このTriggerListenerImplオブジェクトは,下記のようにしてSchedulerに登録する。

Scheduler scheduler = …;
scheduler.addGlobalTriggerListener(new TriggerListenerImpl());

もちろん各Triggerオブジェクト毎にTriggerListenerオブジェクトを登録する方が,TriggerListenerインタフェースの実装クラスの処理内容が簡略化される(Job:Trigger=1:1の場合)。相手が決まっているので,boolean値だけ上げ下げしてあげれば重複起動を抑止できるだろう。しかし,ジョブ定義をXMLファイルで記述し,JobSchedulingDataProcessorクラスを使ってSchedulerオブジェクトに一括登録する場合などは,上記のコードの方が簡潔な実装と言えるだろう。
TriggerListenerという仕組みを通じて,やはりライブラリでもIDEでもフレームワークでも何でも,コールバックの仕組みとその種類の多さ,コールバック時に行える処理の豊富さが,採用頻度を上げるための必須要素だなと改めて認識させてくれた。特に,用意されているコールバックメソッドのセンス,これが重要。QuartzのvetoJobExecution()メソッド,これは久々に僕をビリッと感じさせてくれたメソッドだった。

Get Adobe Flash playerPlugin by wpburn.com wordpress themes