APP下载

用 Quartz 进行作业排程

消息来源:baojiabao.com 作者: 发布时间:2024-06-16

报价宝综合消息用 Quartz 进行作业排程

Quartz API 采用多面方式在 Java 应用程序中进行任务排程

现代的 Web 应用程序框架在范围和复杂性方面都有所发展,应用程序的每个底层元件也必须相应地发展。作业排程是现代系统中对 Java 应用程序的一般要求,而且也是对 Java 开发人员一贯的要求。虽然目前的排程技术比起原始的数据库触发器标志和独立的排程器执行绪来说,已经发展了许多,但是作业排程仍然不是个小问题。对这个问题最合适的解决方案就是来自 OpenSymphony 的 Quartz API。

Quartz 是个开源的作业排程框架,为在 Java 应用程序中进行作业排程提供了简单却强大的机制。Quartz 允许开发人员根据时间间隔(或天)来排程作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了 Quartz 的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业。虽然可以通过属性档案(在属性档案中可以指定 JDBC 事务的资料来源、全域性作业和/或触发器侦听器、外挂、执行绪池,以及更多)配置 Quartz,但它根本没有与应用程序服务器的上下文或引用整合在一起。结果就是作业不能访问 Web 服务器的内部函式;例如,在使用 WebSphere 应用服务器时,由 Quartz 排程的作业并不能影响服务器的动态快取和资料来源。

本文使用一系列程式码示例介绍 Quartz API,演示它的机制,例如作业、触发器、作业仓库和属性。

入门

要开始使用 Quartz,需要用 Quartz API 对专案进行配置。步骤如下:

下载 Quartz API。解压缩并把 quartz-x.x.x.jar 放在专案资料夹内,或者把档案放在专案的类路径中。把 core 和/或 optional 资料夹中的 jar 档案放在专案的资料夹或专案的类路径中。如果使用 JDBCJobStore,把所有的 JDBC jar 档案放在专案的资料夹或专案的类路径中。为了方便读者,我已经把所有必要的档案,包括 DB2 JDBC 档案,编译到一个 zip 档案中。请参阅 下载小节下载程式码。

现在来看一下 Quartz API 的主要元件。

作业和触发器

Quartz 排程包的两个基本单元是作业和触发器。作业 是能够排程的可执行任务,触发器 提供了对作业的排程。虽然这两个实体很容易合在一起,但在 Quartz 中将它们分离开来是有原因的,而且也很有益处。

通过把要执行的工作与它的排程分开,Quartz 允许在不丢失作业本身或作业的上下文的情况下,修改排程触发器。而且,任何单个的作业都可以有多个触发器与其关联。

示例 1:作业

通过实现 org.quartz.job 界面,可以使 Java 类变成可执行的。清单 1 提供了 Quartz 作业的一个示例。这个类用一条非常简单的输出语句覆盖了 execute(JobExecutionContext context) 方法。这个方法可以包含我们想要执行的任何程式码(所有的程式码示例都基于 Quartz 1.5.2,它是编写这篇文章时的稳定发行版)。

清单 1. SimpleQuartzJob.java

package com.ibm.developerworks.quartz;

import java.util.Date;

import org.quartz.Job;

import org.quartz.JobExecutionContext;

import org.quartz.JobExecutionException;

public class SimpleQuartzJob implements Job {

public SimpleQuartzJob() {

}

public void execute(JobExecutionContext context) throws JobExecutionException {

System.out.println("In SimpleQuartzJob - executing its JOB at "

+ new Date() + " by " + context.getTrigger().getName());

}

}

请注意,execute 方法接受一个 JobExecutionContext 物件作为引数。这个物件提供了作业例项的执行时上下文。特别地,它提供了对排程器和触发器的访问,这两者协作来启动作业以及作业的 JobDetail物件的执行。Quartz 通过把作业的状态放在 JobDetail 物件中并让 JobDetail 建构函式启动一个作业的例项,分离了作业的执行和作业周围的状态。JobDetail 物件储存作业的侦听器、群组、资料对映、描述以及作业的其他属性。

示例 2:简单触发器

触发器可以实现对任务执行的排程。Quartz 提供了几种不同的触发器,复杂程度各不相同。清单 2 中的 SimpleTrigger 展示了触发器的基础:

清单 2. SimpleTriggerRunner.java

public void task() throws SchedulerException

{

// Initiate a Schedule Factory

SchedulerFactory schedulerFactory = new StdSchedulerFactory();

// Retrieve a scheduler from schedule factory

Scheduler scheduler = schedulerFactory.getScheduler();

// current time

long ctime = System.currentTimeMillis();

// Initiate JobDetail with job name, job group, and executable job class

JobDetail jobDetail =

new JobDetail("jobDetail-s1", "jobDetailGroup-s1", SimpleQuartzJob.class);

// Initiate SimpleTrigger with its name and group name

SimpleTrigger simpleTrigger =

new SimpleTrigger("simpleTrigger", "triggerGroup-s1");

// set its start up time

simpleTrigger.setStartTime(new Date(ctime));

// set the interval, how often the job should run (10 seconds here)

simpleTrigger.setRepeatInterval(10000);

// set the number of execution of this job, set to 10 times.

// It will run 10 time and exhaust.

simpleTrigger.setRepeatCount(100);

// set the ending time of this job.

// We set it for 60 seconds from its startup time here

// Even if we set its repeat count to 10,

// this will stop its process after 6 repeats as it gets it endtime by then.

//simpleTrigger.setEndTime(new Date(ctime + 60000L));

// set priority of trigger. If not set, the default is 5

//simpleTrigger.setPriority(10);

// schedule a job with JobDetail and Trigger

scheduler.scheduleJob(jobDetail, simpleTrigger);

// start the scheduler

scheduler.start();

}

清单 2 开始时例项化一个 SchedulerFactory,获得此排程器。就像前面讨论过的,建立 JobDetail 物件时,它的建构函式要接受一个 Job 作为引数。顾名思义,SimpleTrigger 例项相当原始。在建立物件之后,设定几个基本属性以立即排程任务,然后每 10 秒重复一次,直到作业被执行 100 次。

还有其他许多方式可以操纵 SimpleTrigger。除了指定重复次数和重复间隔,还可以指定作业在特定日历时间执行,只需给定执行的最长时间或者优先级(稍后讨论)。执行的最长时间可以覆盖指定的重复次数,从而确保作业的执行不会超过最长时间。

示例 3: Cron 触发器

CronTrigger 支援比 SimpleTrigger 更具体的排程,而且也不是很复杂。基于 cron 表示式,CronTrigger 支援类似日历的重复间隔,而不是单一的时间间隔 —— 这相对 SimpleTrigger 而言是一大改进。

Cron 表示式包括以下 7 个字段:

秒分小时月内日期月周内日期年(可选字段)特殊字元

Cron 触发器利用一系列特殊字元,如下所示:

反斜线(/)字元表示增量值。例如,在秒字段中“5/15”代表从第 5 秒开始,每 15 秒一次。问号(?)字元和字母 L 字元只有在月内日期和周内日期字段中可用。问号表示这个字段不包含具体值。所以,如果指定月内日期,可以在周内日期字段中插入“?”,表示周内日期值无关紧要。字母 L 字元是 last 的缩写。放在月内日期字段中,表示安排在当月最后一天执行。在周内日期字段中,如果“L”单独存在,就等于“7”,否则代表当月内周内日期的最后一个例项。所以“0L”表示安排在当月的最后一个星期日执行。在月内日期字段中的字母(W)字元把执行安排在最靠近指定值的工作日。把“1W”放在月内日期字段中,表示把执行安排在当月的第一个工作日内。井号(#)字元为给定月份指定具体的工作日例项。把“MON#2”放在周内日期字段中,表示把任务安排在当月的第二个星期一。星号(*)字元是通配字元,表示该字段可以接受任何可能的值。所有这些定义看起来可能有些吓人,但是只要几分钟练习之后,cron 表示式就会显得十分简单。

清单 3 显示了 CronTrigger 的一个示例。请注意 SchedulerFactory、Scheduler 和 JobDetail 的例项化,与 SimpleTrigger 示例中的例项化是相同的。在这个示例中,只是修改了触发器。这里指定的 cron 表示式(“0/5 * * * * ?”)安排任务每 5 秒执行一次。

清单 3. CronTriggerRunner.java

public void task() throws SchedulerException

{

// Initiate a Schedule Factory

SchedulerFactory schedulerFactory = new StdSchedulerFactory();

// Retrieve a scheduler from schedule factory

Scheduler scheduler = schedulerFactory.getScheduler();

// current time

long ctime = System.currentTimeMillis();

// Initiate JobDetail with job name, job group, and executable job class

JobDetail jobDetail =

new JobDetail("jobDetail2", "jobDetailGroup2", SimpleQuartzJob.class);

// Initiate CronTrigger with its name and group name

CronTrigger cronTrigger = new CronTrigger("cronTrigger", "triggerGroup2");

try {

// setup CronExpression

CronExpression cexp = new CronExpression("0/5 * * * * ?");

// Assign the CronExpression to CronTrigger

cronTrigger.setCronExpression(cexp);

} catch (Exception e) {

e.printStackTrace();

}

// schedule a job with JobDetail and Trigger

scheduler.scheduleJob(jobDetail, cronTrigger);

// start the scheduler

scheduler.start();

}

高阶 Quartz

如上所示,只用作业和触发器,就能访问大量的功能。但是,Quartz 是个丰富而灵活的排程包,对于愿意研究它的人来说,它还提供了更多功能。下一节讨论 Quartz 的一些高阶特性。

作业仓库

Quartz 提供了两种不同的方式用来把与作业和触发器有关的资料储存在内存或数据库中。第一种方式是 RAMJobStore 类的例项,这是预设设定。这个作业仓库最易使用,而且提供了最佳效能,因为所有资料都储存在内存中。这个方法的主要不足是缺乏资料的永续性。因为资料储存在 RAM 中,所以应用程序或系统崩溃时,所有资讯都会丢失。

为了修正这个问题,Quartz 提供了 JDBCJobStore。顾名思义,作业仓库通过 JDBC 把所有资料放在数据库中。资料永续性的代价就是效能降低和复杂性的提高。

设定 JDBCJobStore

在前面的示例中,已经看到了 RAMJobStore 例项的工作情况。因为它是预设的作业仓库,所以显然不需要额外设定就能使用它。但是,使用 JDBCJobStore 需要一些初始化。

在应用程序中设定使用 JDBCJobStore 需要两步:首先必须建立作业仓库使用的数据库表。JDBCJobStore 与所有主流数据库都相容,而且 Quartz 提供了一系列建立表的 SQL 指令码,能够简化设定过程。可以在 Quartz 发行包的 “docs/dbTables”目录中找到建立表的 SQL 指令码。第二,必须定义一些属性,如表 1 所示:

表 1. JDBCJobStore 属性

属性名称值org.quartz.jobStore.classorg.quartz.impl.jdbcjobstore.JobStoreTX (or JobStoreCMT)org.quartz.jobStore.tablePrefixQRTZ_ (optional, customizable)org.quartz.jobStore.driverDelegateClassorg.quartz.impl.jdbcjobstore.StdJDBCDelegateorg.quartz.jobStore.dataSourceqzDS (customizable)org.quartz.dataSource.qzDS.drivercom.ibm.db2.jcc.DB2Driver (could be any other database driver)org.quartz.dataSource.qzDS.urljdbc:db2://localhost:50000/QZ_SMPL (customizable)org.quartz.dataSource.qzDS.userdb2inst1 (place userid for your own db)org.quartz.dataSource.qzDS.passwordpass4dbadmin (place your own password for user)org.quartz.dataSource.qzDS.maxConnections30

清单 4 展示了 JDBCJobStore 提供的资料永续性。就像在前面的示例中一样,先从初始化 SchedulerFactory 和 Scheduler 开始。然后,不再需要初始化作业和触发器,而是要获取触发器群组名称列表,之后对于每个群组名称,获取触发器名称列表。请注意,每个现有的作业都应当用 Scheduler.reschedule() 方法重新排程。仅仅重新初始化在先前的应用程序执行时终止的作业,不会正确地装载触发器的属性。

清单 4. JDBCJobStoreRunner.java

public void task() throws SchedulerException

{

// Initiate a Schedule Factory

SchedulerFactory schedulerFactory = new StdSchedulerFactory();

// Retrieve a scheduler from schedule factory

Scheduler scheduler = schedulerFactory.getScheduler();

String[] triggerGroups;

String[] triggers;

triggerGroups = scheduler.getTriggerGroupNames();

for (int i = 0; i triggers = scheduler.getTriggerNames(triggerGroups[i]);

for (int j = 0; j Trigger tg = scheduler.getTrigger(triggers[j], triggerGroups[i]);

if (tg instanceof SimpleTrigger && tg.getName().equals("simpleTrigger")) {

((SimpleTrigger)tg).setRepeatCount(100);

// reschedule the job

scheduler.rescheduleJob(triggers[j], triggerGroups[i], tg);

// unschedule the job

//scheduler.unscheduleJob(triggersInGroup[j], triggerGroups[i]);

}

}

}

// start the scheduler

scheduler.start();

}

执行 JDBCJobStore

在第一次执行示例时,触发器在数据库中初始化。图 1 显示了数据库在触发器初始化之后但尚未击发之前的情况。所以,基于 清单 4 中的 setRepeatCount() 方法,将 REPEAT_COUNT 设为 100,而 TIMES_TRIGGERED 是 0。在应用程序执行一段时间之后,应用程序停止。

图 1. 使用 JDBCJobStore 时数据库中的资料(执行前)

图 2 显示了数据库在应用程序停止后的情况。在这个图中,TIMES_TRIGGERED 被设为 19,表示作业执行的次数。

图 2. 同一资料在 19 次迭代之后

当再次启动应用程序时,REPEAT_COUNT 被更新。这在图 3 中很明显。在图 3 中可以看到 REPEAT_COUNT被更新为 81,所以新的 REPEAT_COUNT 等于前面的 REPEAT_COUNT 值减去前面的 TIMES_TRIGGERED 值。而且,在图 3 中还看到新的 TIMES_TRIGGERED 值是 7,表示作业从应用程序重新启动以来,又触发了 7 次。

图 3. 第 2 次执行 7 次迭代之后的资料

当再次停止应用程序之后,REPEAT_COUNT 值再次更新。如图 4 所示,应用程序已经停止,还没有重新启动。同样,REPEAT_COUNT 值更新成前一个 REPEAT_COUNT 值减去前一个 TIMES_TRIGGERED 值。

图 4. 再次执行触发器之前的初始资料

使用属性

正如在使用 JDBCJobStore 时看到的,可以用许多属性来调整 Quartz 的行为。应当在 quartz.properties档案中指定这些属性。请参阅 参考资料 获得可以配置的属性的列表。清单 5 显示了用于 JDBCJobStore示例的属性:

清单 5. quartz.properties

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

org.quartz.threadPool.threadCount = 10

org.quartz.threadPool.threadPriority = 5

org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

# Using RAMJobStore

## if using RAMJobStore, please be sure that you comment out the following

## - org.quartz.jobStore.tablePrefix,

## - org.quartz.jobStore.driverDelegateClass,

## - org.quartz.jobStore.dataSource

#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

# Using JobStoreTX

## Be sure to run the appropriate script(under docs/dbTables) first to create tables

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

# Configuring JDBCJobStore with the Table Prefix

org.quartz.jobStore.tablePrefix = QRTZ_

# Using DriverDelegate

org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

# Using datasource

org.quartz.jobStore.dataSource = qzDS

# Define the datasource to use

org.quartz.dataSource.qzDS.driver = com.ibm.db2.jcc.DB2Driver

org.quartz.dataSource.qzDS.URL = jdbc:db2://localhost:50000/dbname

org.quartz.dataSource.qzDS.user = dbuserid

org.quartz.dataSource.qzDS.password = password

org.quartz.dataSource.qzDS.maxConnections = 30

结束语

Quartz 作业排程框架所提供的 API 在两方面都表现极佳:既全面强大,又易于使用。Quartz 可以用于简单的作业触发,也可以用于复杂的 JDBC 持久的作业储存和执行。OpenSymphony 在开放源代码世界中成功地填补了一个空白,过去繁琐的作业排程现在对开发人员来说不过是小菜一碟。

2019-10-18 15:53:00

相关文章