Quartz Job Scheduling Framework[翻译]第七章. 实现 Quartz 监听器 (第二部分)

译者:Unmi(隔叶黄莺)
http://www.blogjava.net/Unmi/archive/2008/07/08/213435.html

五. 监听 Scheduler 事件

org.quartz.SchedulerListener 接口包含了一系列的回调方法,它们会在 Scheduler 的生命周期中有关键事件发生时被调用。代码 7.9 列出了包括在 SchedulerListener 接口的方法。

代码 7.9. org.quartz.SchedulerListener 接口中的方法

public interface SchedulerListener {   
    public void jobScheduled(Trigger trigger);   
    public void jobUnscheduled(String triggerName, String triggerGroup);   
    public void triggerFinalized(Trigger trigger);   
    public void triggersPaused(String triggerName, String triggerGroup);   
    public void triggersResumed(String triggerName,String triggerGroup);   
    public void jobsPaused(String jobName, String jobGroup);   
    public void jobsResumed(String jobName, String jobGroup);   
    public void schedulerError(String msg, SchedulerException cause);   
    public void schedulerShutdown();   
}  


正如你从代码 7.9 中列示看到的方法那样,SchedulerListener 是在 Scheduler 级别的事件产生时得到通知,不管是增加还是移除 Scheduler 中的 Job,或者是 Scheduler 遭遇到了严重的错误时。那些事件多是关于对 Scheduler 管理的,而不是专注于 Job 或 Trigger 的。

· jobScheduled() 和 jobUnscheduled() 方法

Scheduler 在有新的 JobDetail 部署或卸载时调用这两个中的相应方法。

· triggerFinalized() 方法

当一个 Trigger 来到了再也不会触发的状态时调用这个方法。除非这个 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。

·triggersPaused() 方法

Scheduler 调用这个方法是发生在一个 Trigger 或 Trigger 组被暂停时。假如是 Trigger 组的话,triggerName 参数将为 null。

·triggersResumed() 方法

Scheduler 调用这个方法是发生成一个 Trigger 或 Trigger 组从暂停中恢复时。假如是 Trigger 组的话,triggerName 参数将为 null。

·jobsPaused() 方法

当一个或一组 JobDetail 暂停时调用这个方法。

·jobsResumed() 方法

当一个或一组 Job 从暂停上恢复时调用这个方法。假如是一个 Job 组,jobName 参数将为 null。

·schedulerError() 方法

在 Scheduler 的正常运行期间产生一个严重错误时调用这个方法。错误的类型会各式的,但是下面列举了一些错误例子:

·初始化 Job 类的问题

·试图去找到下一 Trigger 的问题

·JobStore 中重复的问题

·数据存储连接的问题

你可以使用 SchedulerException 的 getErrorCode() 或者 getUnderlyingException() 方法或获取到特定错误的更详尽的信息。

·schedulerShutdown() 方法

Scheduler 调用这个方法用来通知 SchedulerListener Scheduler 将要被关闭。

代码 7.10 展示了一个 SchedulerListener 实现
 
package org.cavaness.quartzbook.chapter7;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.SchedulerException;
import org.quartz.SchedulerListener;
import org.quartz.Trigger;
public class SimpleSchedulerListener implements SchedulerListener {
  Log logger = LogFactory.getLog(SimpleSchedulerListener.class);
  public void jobScheduled(Trigger trigger) {
    String jobName = trigger.getJobName();
    logger.info(jobName + " has been scheduled");
  }
  public void jobUnscheduled(String triggerName, String triggerGroup) {
    if (triggerName == null) {
      // triggerGroup is being unscheduled
      logger.info(triggerGroup + " is being unscheduled");
    } else {
      logger.info(triggerName + " is being unscheduled");
    }
  }
  public void triggerFinalized(Trigger trigger) {
    String jobName = trigger.getJobName();
    logger.info("Trigger is finished for " + jobName);
  }
  public void triggersPaused(String triggerName, String triggerGroup) {
    if (triggerName == null) {
      // triggerGroup is being unscheduled
      logger.info(triggerGroup + " is being paused");
    } else {
      logger.info(triggerName + " is being paused");
    }
  }
  public void triggersResumed(String triggerName, String triggerGroup) {
    if (triggerName == null) {
      // triggerGroup is being unscheduled
      logger.info(triggerGroup + " is now resuming");
    } else {
      logger.info(triggerName + " is now resuming");
    }
  }
  public void jobsPaused(String jobName, String jobGroup) {
    if (jobName == null) {
      // triggerGroup is being unscheduled
      logger.info(jobGroup + " is pausing");
    } else {
      logger.info(jobName + " is pausing");
    }
  }
  public void jobsResumed(String jobName, String jobGroup) {
    if (jobName == null) {
      // triggerGroup is being unscheduled
      logger.info(jobGroup + " is now resuming");
    } else {
      logger.info(jobName + " is now resuming");
    }
  }
  public void schedulerError(String msg, SchedulerException cause) {
    logger.error(msg, cause.getUnderlyingException());
  }
  public void schedulerShutdown() {
    logger.info("Scheduler is being shutdown");
  }
}


和前面的例子一样,代码 7.10 中的 SimpleSchedulerListener 只提供了监听方法的简单实现。

代码 7.11 使用了 SimpleSchedulerListener 类。

 
package org.cavaness.quartzbook.chapter7;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cavaness.quartzbook.common.PrintInfoJob;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerListener;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
public class Listing_7_11 {
  static Log logger = LogFactory.getLog(Listing_7_11.class);
  public static void main(String[] args) {
    Listing_7_11 example = new Listing_7_11();
    try {
      example.startScheduler();
    } catch (SchedulerException ex) {
      logger.error(ex);
    }
  }
  public void startScheduler() throws SchedulerException {
    // Create an instance of the factory
    Scheduler scheduler = null;
    // Create the scheduler and JobDetail
    scheduler = StdSchedulerFactory.getDefaultScheduler();
    // Create and register the scheduler listener
    SchedulerListener schedulerListener = new SimpleSchedulerListener();
    scheduler.addSchedulerListener(schedulerListener);
    // Start the scheduler
    scheduler.start();
    logger.info("Scheduler was started at " + new Date());
    // Create the JobDetail
    JobDetail jobDetail = new JobDetail("PrintInfoJob", Scheduler.DEFAULT_GROUP, PrintInfoJob.class);
    /*
     * Set up a trigger to start firing now, with no end date/time, repeat forever and have 5 secs between each firing.
     */
    Trigger trigger = TriggerUtils.makeSecondlyTrigger(5);
    trigger.setName("SimpleTrigger");
    trigger.setStartTime(new Date());
    // Register the JobDetail and Trigger
    scheduler.scheduleJob(jobDetail, trigger);
  }
}



相比于前面的例子,我们在代码 7.11 中作了些小许改动,来实际促使更多的 SchedulerListener 方法被调用。在代码 7.11 中,Scheduler 创建后是在 Job 注册之前被启动的。这就使得在 Job 部署时 jobScheduled() 方法能得到调用。我们也改变了 Trigger 只重复两次而不是无限的运行。这样能强制 triggerFinalized() 方法被调用,因为这个 Trigger 不再有机会触发了。除了这些人为的条件外,使用 SchedulerListener 就和使用 Job 或 Trigger 监听器是一样的了。


六. 使用 FileScanListener

Quartz 框架还包含一个我们未曾提及的监听器。这个监听器不像别的,因为它是为特定目的而设计的:同框架所带的一个工具 Job 一起用的。

这个监听器就是 org.quartz.jobs.FileScanListener 接口,它显式的设计为 FileScanJob 所用的,这一 Job 也在 org.quartz.jobs 包中。FileScanJob 检查某一指定文件的 lastModifiedDate。当某人改变了这个文件,这个 Job 就调用 FileScanListener 的 fileUpdated() 方法。

就像使用其他类型的 Quartz 监听器一样,你必须创建一个实现了 FileScanListener 接口的具体类。只有一个方法需要实现:

public void fileUpdated(String fileName);

代码 7.12 展了了我们的一个极简单的 FileScanListener 实现。

 
package org.cavaness.quartzbook.chapter7;
import java.io.File;
import java.sql.Timestamp;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class SimpleFileScanListener implements org.quartz.jobs.FileScanListener {
  private static Log logger = LogFactory.getLog(SimpleFileScanListener.class);
  public void fileUpdated(String fileName) {
    File file = new File(fileName);
    Timestamp modified = new Timestamp(file.lastModified());
    logger.info(fileName + " was changed at " + modified);
  }
}

显然,你会想做些更有意义的事情,而不仅仅是写下一条日志信息,但是你从代码 7.12 中的简单例子中明白了要旨。我们也必须在部署 FileScanJob 时为它使用这一新型监听器。

代码 7.13 展示了如何部署 FileScanJob。

 
package org.cavaness.quartzbook.chapter7;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.jobs.FileScanJob;
public class Listing_7_13 {
  private static Log logger = LogFactory.getLog(Listing_7_13.class);
  public static void main(String[] args) {
    Listing_7_13 example = new Listing_7_13();
    try {
      Scheduler scheduler = example.createScheduler();
      example.scheduleJob(scheduler);
      scheduler.start();
    } catch (SchedulerException ex) {
      logger.error(ex);
    }
  }
  protected Scheduler createScheduler() throws SchedulerException {
    return StdSchedulerFactory.getDefaultScheduler();
  }
  protected void scheduleJob(Scheduler scheduler) throws SchedulerException {
    // Store the FileScanListener instance
    scheduler.getContext().put("SimpleFileScanListener", new SimpleFileScanListener());
    // Create a JobDetail for the FileScanJob
    JobDetail jobDetail = new JobDetail("FileScanJob", null, FileScanJob.class);
    // The FileScanJob needs some parameters
    JobDataMap jobDataMap = new JobDataMap();
    jobDataMap.put(FileScanJob.FILE_NAME, "C:\\quartz-book\\input1\\test.txt");
    jobDataMap.put(FileScanJob.FILE_SCAN_LISTENER_NAME, "SimpleFileScanListener");
    jobDetail.setJobDataMap(jobDataMap);
    // Create a Trigger and register the Job
    Trigger trigger = TriggerUtils.makeSecondlyTrigger(30);
    trigger.setName("SimpleTrigger");
    trigger.setStartTime(new Date());
    scheduler.scheduleJob(jobDetail, trigger);
  }
}


代码 7.13 中的程序像几乎所有别的 必须部署一个 Job 的 Quartz 应用。FileScanJob 需要两个参数:要监视文件的 FILE_NAME,和 FileScanListener(FILE_SCAN_LISTENER_NAME) 的名称。这两个参数的值会存在 JobDataMap 中,因此 FileScanJob 能访问到它们。

仅有一个特别要注意,容易出错的地方就是要确保添加了一个 FileScanListener 到 SchedulerContext 中。这在代码 7.13 中是通过如下代码片断完成的:

scheduler.getContext().put("SimpleFileScanListener",
     new SimpleFileScanListener());


这一步是必须的,因为 FileScanJob 获取到 SchedulerContext 的引用,然后使用设置到 JobDataMap 中的名称找寻 FileScanListener。

jobDataMap.put(FileScanJob.FILE_SCAN_LISTENER_NAME,
     "SimpleFileScanListener");


如果你还有所困惑,别担心:看一看 org.quartz.jobs.FileScanJob 类的源代码吧。这是对待开源软件最好的方式了。



七. 在 quartz_jobs.xml 文件中实现监听器

本章的所有例子告诉了你如何以编程的方式设置监听器。假如我们一个关于在 quartz_jobs.xml 文件中以声明式配置监听器的例子都不提供本章就不能算是完结。

自 Quartz 1.5 开始,你能够在 Job 定义文件中指定监听器,当然就是知名的 quartz_jobs.xml 文件了。

代码 7.14. Quartz 监听器能在 quartz_jobs.xml 文件中实现

<?xml version='1.0' encoding='utf-8'?>
<quartz>
  <job-listener class-name="org.cavaness.quartzbook.chapter7.SimpleJobListener" name="SimpleJobListener"></job-listener>
  <job>
    <job-detail>
      <name>PrintInfoJob</name>
      <group>DEFAULT</group>
      <job-listener-ref>SimpleJobListener</job-listener-ref>
      <job-class>org.cavaness.quartzbook.common.PrintInfoJob</job-class>
    </job-detail>
    <trigger>
      <simple>
        <name>printJobTrigger</name>
        <group>DEFAULT</group>
        <job-name>PrintInfoJob</job-name>
        <job-group>DEFAULT</job-group>
        <start-time>2005-09-13 6:10:00 PM</start-time>
        <! repeat indefinitely every 10 seconds  
     
        <repeat-count>-1</repeat-count>  
     <repeat-interval>10000</repeat-interval>  
   </simple>
    </trigger>
  </job>
</quartz>



在代码 7.14 中你看到那个附加的 <job-listener> 元素,它有两个必须的属性:

<job-listener
  class-name="org.cavaness.quartzbook.chapter7.SimpleJobListener"
  name="SimpleJobListener">


class-name 属性标识了监听器类的全限名称。name 属性指派给这个监听器一个逻辑名,在 <job-detail> 元素中用到。

下一步就是为在同一个文件中的每一个要用到监听器的 <job-detail> 元素中定义一个 <job-listener-ref> 元素。该元素的值必须与文件中所定义的其中一个 <job-listener> 元素的 name 属性相匹配。

做完那之后,要确保你已经在 quartz.properties 文件中通过设置属性让 Scheduler 使用了 JobInitializationPlugin。Quartz 插件会在下一章中详细讨论。目前,只要加入以下行到你的 quartz.properties 文件中:

org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin

org.quartz.plugin.jobInitializer.overWriteExistingJobs = true
org.quartz.plugin.jobInitializer.failOnFileNotFound = true
org.quartz.plugin.jobInitializer.validating=false


然后命名你的 XML 文件为 quartz_jobs.xml 并放到你的 classpath 下。


一些需注意的易出错的地方

很有必要提醒你在尝试 XML 文件中设置监听器时很可能会遇到的两个问题。在 Quartz 1.5 中,至少,监听器的的 setName() 方法未包含中接口中。getName() 方法是有的,但不存在相应的 setName()。这在以编程式使用监听器时似乎不会导致什么问题,但是对于声明式时就有问题了。你需要简单的为你的监听器创建一个 setName() 方法。

另一个提示就是要确保你的监听器有一个无参构造方法。在某些情况下,Quartz 框架不会在乎,但当以声明式使用它时,你将会得到一个错误。最好还是声明一个无参的构造方法,这样总是安全的。


--------------------------------------------------------------------------------
[译者 Unmi 后记]
配置文件中写成 <start-time>2005-09-13 6:10:00 PM</start-time>,让 Quartz 来解析这个时间字符串可能会出现异常,这在我的一个回复(http://www.blogjava.net/Unmi/archive/2007/11/17/159830.html#210032) 中有论及,现摘录如下:

quart_jobx.xml 中时间格式的问题,写成 2008-06-20 7:23:00 PM 的话 JobSchedulingDataProcessor.parseDate(value) 没办法解析

在 quarts_jobs.xml 中 <start-time> 的格式是:

<start-time>2008-06-23T21:23:00</start-time>

T隔开日期和时间,默认时区

或者:
<start-time>2008-06-23T21:23:00+08:00</start-time>
+08:00 表示东八区

我觉得这是 Quartz 的一个 Bug,其实 Quartz 在解析时间时准备了两个 Pattern 的,分别是:
yyyy-MM-dd'T'hh:mm:ss
yyyy-MM-dd hh:mm:ss a

但是在 JobSchedulingDataProcessor.parseDate(value) 方法中只会以第一个 Pattern 解析时间,并不会尝试使用第二个 Pattern 去解析时间,第二个 Pattern 是可以认识 2008-06-20 7:23:00 PM 的。

所以为了规避这个问题,还是应该写成 yyyy-MM-dd'T'hh:mm:ss 格式。



八. 监听器中的线程使用

你看到了监听器接口中的方法后,你或许想知道是线程在调用监听器方法中饰演着什么样的角色。基实监听器方法是存在一个时序的,正如你看到方法名能想像到的那样。在一个 Job 执行的生命周期中,调用监听器方法以的顺序通常是固定的。图 7.2 描绘了监听方法的调用顺序和所涉及到的工作者线程。


图 7.2. 监听器方法按某一特定的时序被调用

调用监听器方法的时序是固定的。如图 7.2 所示,在 Job 的执行前后,调用 Job 的 execute() 方法相同的线程被用于调用 JobListener 和 TriggerListener 的方法。假如你使用任何类类型的第三方线程管理工具或者打算实现你自己的线程池管理,知道这一点是很重要的。假如你在监听方法中实现了一个长运行逻辑时,这也会带来对性能上的负面影响。因为调用监听方法的线程和执行 Job 是同一个工作者线程,你不应该把监听方法实现的太复杂并要花费较长时间才能完成。保持它们的执行时间尽可能短。

九. Quartz 监听器的使用

了解了这所有的知识之后,那你能拿这些监听器做什么呢?实际上,你可以做相当多的事情。首先,值得注意的是,在内部,Quartz 使用这些监听器来帮助管理 Scheduler 和你的 Job 和 Trigger。框架还包含两个实现了监听器接口的插件,它们记录所有 Job 的日志和触发历史:分别为 org.quartz.plugins.history.LoggingJobHistoryPlugin 和 org.quartz.plugins.history.LogginTriggerHistoryPlugin。我们在下一章讲到 Quartz 插件。

这里有一些监听的使用:

·捕获错过触发和重新的部署

·成功执行完一个 Job 后发送一个 e-mail

·基于数据库中设置的标记否决 Job 的执行

·基于一个 Job 执行的成功或失败部署其他的 Job

·记录一个 Job 的实际运行时间

这些仅是一些想法。当你的 Quartz 应用运行期间出现特定的事件时,Quartz 监听器为你提供了一种方法接收到编程角度上的通知。你选择什么来应用那些知道,如果有的话,完全由你而定。
QuartzFigure7_2.jpg
快乐渡过每一天,减肥坚持每一天