[置顶] quartznet任务调度和消息调度(JAVA与C#版对比)

时间:2023-03-09 00:42:11
[置顶] quartznet任务调度和消息调度(JAVA与C#版对比)

quartznet任务调度和消息调度

1.  作用

自动执行任务。

2.  下载地址

NET版本

JAVA版本

1下载

http://quartznet.sourceforge.net/download.html

http://opensymphony.com/quartz

2工具

Visual Studio2008/2010打开

3概念

调度器和作业

调度器、任务和触发器

1)  作业

是一个执行任务的简单.NET类。任务可以是任何C#\VB.NET代码。只需你实现Quartz.IJob接口并且在出现严重错误情况下抛出JobExecutionException异常即可。

IJob接口包含唯一的一个方法Execute()。

调度器负责管理Quartz.NET应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz.NET采用了基于多线程的架构。启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业。这就是Quartz.NET怎样能并发运行多个作业的原理。Quartz.NET依赖一套松耦合的线程池管理部件来管理线程环境。

1)Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;

2)JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

通过该类的构造函数可以更具体地了解它的功用:JobDetail(java.lang.String name, java.lang.String group,  java.lang.Class jobClass),该构造函数要求指定Job的实现类,以及任务在Scheduler中的组名和Job名称;

3)Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;

4)Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。

假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。针对不同时间段类型,Quartz在org.quartz.impl.calendar包下提供了若干个Calendar的实现类,如AnnualCalendar、MonthlyCalendar、WeeklyCalendar分别针对每年、每月和每周进行定义;

5)Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。

6)ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

作业管理和存储:Quartz通过一个称之为作业存储(JobStore)的概念来做作业存储和管理。

有效作业存储:

1)作业内存存储类型RAMJobStore(缺省)

)数据库存储AdoJobStore

A)支持AdoJobStore几乎可以在任何数据库上工作,它广泛地使用Oracle, MySQL, MS SQLServer2000, HSQLDB, PostreSQL以及 DB2。

B)脚本在:AdoJobStore几乎可以在任何数据库上工作,它广泛地使用Oracle, MySQL, MS SQLServer2000, HSQLDB, PostreSQL 以及 DB2。

C)需要注意的一件事情就是所有Quartz库表名都以QRTZ_作为前缀(例如:表"QRTZ_TRIGGERS",及"QRTZ_JOB_DETAIL")。实际上,可以你可以将前缀设置为任何你想要的前缀,只要你告诉AdoJobStore那个前缀是什么即可(在你的Quartz属性文件中配置)。对于一个数据库中使用多个scheduler实例,那么配置不同的前缀可以创建多套库表,十分有用。

3.  C#应用

Quartz.NET是一个开源的作业调度框架,是OpenSymphony的 Quartz API的.NET移植,它用C#写成,可用于winform和asp.net应用中。它提供了巨大的灵活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,支持cron-like表达式等等。

你曾经需要应用执行一个任务吗?这个任务每天或每周星期二晚上11:30,或许仅仅每个月的最后一天执行。一个自动执行而无须干预的任务在执行过程中如果发生一个严重错误,应用能够知到其执行失败并尝试重新执行吗?你和你的团队是用.NET编程吗?如果这些问题中任何一个你回答是,那么你应该使用Quartz.NET调度器。 Quartz.NET允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了 Quartz.NET的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业.

Quartz.NET入门

要开始使用 Quartz.NET,需要用 Quartz.NET API 对项目进行配置。步骤如下:

1.到http://quartznet.sourceforge.net/download.html下载 Quartz.NET API,最新版本是0.6

2.解压缩Quartz.NET-0.6.zip到目录,根据你的项目情况用Visual Studio 2003或者Visual Studio 2005打开相应工程,编译。你可以将它放进自己的应用中。Quartz.NET框架只需要少数的第三方库,并且这些三方库是必需的,你很可能已经在使用这些库了。

3.在Quartz.NET有一个叫做quartz.properties的配置文件,它允许你修改框架运行时环境。缺省是使用Quartz.dll里面的quartz.properties文件。当然你可以在应用程序配置文件中做相应的配置,下面是一个配置文件示例:

<?xml version="1.0"encoding="utf-8"?>

<configuration>

<configSections>

<section name="quartz" type="System.Configuration.NameValueSectionHandler,System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089"/>

</configSections>

<quartz>

<add key="quartz.scheduler.instanceName" value="ExampleDefaultQuartzScheduler"/>

<add key="quartz.threadPool.type" value="Quartz.Simpl.SimpleThreadPool,Quartz"/>

<add key="quartz.threadPool.threadCount" value="10"/>

<add key="quartz.threadPool.threadPriority" value="2"/>

<add key="quartz.jobStore.misfireThreshold" value="60000"/>

<add key="quartz.jobStore.type" value="Quartz.Simpl.RAMJobStore,Quartz"/>

</quartz>

</configuration>

为了方便读者,我们使用Quartz.NET的例子代码来解释,现在来看一下 Quartz API 的主要组件。

调度器和作业

Quartz.NET框架的核心是调度器。调度器负责管理Quartz.NET应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz.NET采用了基于多线程的架构。启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业。这就是Quartz.NET怎样能并发运行多个作业的原理。Quartz.NET依赖一套松耦合的线程池管理部件来管理线程环境。作业是一个执行任务的简单.NET类。任务可以是任何C#\VB.NET代码。只需你实现Quartz.IJob接口并且在出现严重错误情况下抛出JobExecutionException异常即可。

IJob接口包含唯一的一个方法Execute(),作业从这里开始执行。一旦实现了IJob接口和Execute ()方法,当Quartz.NET确定该是作业运行的时候,它将调用你的作业。Execute()方法内就完全是你要做的事情。

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

清单 1:作业

using System;

using System.Collections.Generic;

using System.Text;

using Common.Logging;

using Quartz;

namespace QuartzBeginnerExample

{

publicclass SimpleQuartzJob : IJob

{

privatestatic ILog _log = LogManager.GetLogger(typeof(SimpleQuartzJob));

///<summary>

/// Called by the<seecref="IScheduler" /> whena

///<seecref="Trigger" /> firesthat is associated with

/// the <seecref="IJob" />.

///</summary>

publicvirtualvoid Execute(JobExecutionContext context)

{

try

{

// This job simply prints out its job nameand the

// date and time that it is running

string jobName = context.JobDetail.FullName;

_log.Info("Executing job:" + jobName +" executing at " + DateTime.Now.ToString("r"));

}

catch (Exception e)

{

_log.Info("--- Error injob!");

JobExecutionException e2 = new JobExecutionException(e);

// this job will refire immediately

e2.RefireImmediately = true;

throw e2;

}

}

}

}

请注意,Execute方法接受一个 JobExecutionContext 对象作为参数。这个对象提供了作业实例的运行时上下文。特别地,它提供了对调度器和触发器的访问,这两者协作来启动作业以及作业的 JobDetail 对象的执行。Quartz.NET 通过把作业的状态放在 JobDetail 对象中并让 JobDetail 构造函数启动一个作业的实例,分离了作业的执行和作业周围的状态。JobDetail 对象储存作业的侦听器、群组、数据映射、描述以及作业的其他属性。

作业和触发器:

Quartz.NET设计者做了一个设计选择来从调度分离开作业。Quartz.NET中的触发器用来告诉调度程序作业什么时候触发。框架提供了一把触发器类型,但两个最常用的是SimpleTrigger和CronTrigger。SimpleTrigger为需要简单打火调度而设计。

典型地,如果你需要在给定的时间和重复次数或者两次打火之间等待的秒数打火一个作业,那么SimpleTrigger适合你。另一方面,如果你有许多复杂的作业调度,那么或许需要CronTrigger。

CronTrigger是基于Calendar-like调度的。当你需要在除星期六和星期天外的每天上午10点半执行作业时,那么应该使用CronTrigger。正如它的名字所暗示的那样,CronTrigger是基于Unix克隆表达式的。

Cron表达式被用来配置CronTrigger实例。Cron表达式是一个由7个子表达式组成的字符串。每个子表达式都描述了一个单独的日程细节。这些子表达式用空格分隔,分别表示:

1. Seconds秒

2. Minutes分钟

3. Hours小时

4. Day-of-Month月中的天

5. Month月

6. Day-of-Week周中的天

7. Year (optional field)年(可选的域)

一个cron表达式的例子字符串为"0 0 12 ? * WED",这表示“每周三的中午12:00”。

单个子表达式可以包含范围或者列表。例如:前面例子中的周中的天这个域(这里是"WED")可以被替换为"MON-FRI","MON, WED, FRI"或者甚至"MON-WED,SAT"。

通配符('*')可以被用来表示域中“每个”可能的值。因此在"Month"域中的*表示每个月,而在Day-Of-Week域中的*则表示“周中的每一天”。

所有的域中的值都有特定的合法范围,这些值的合法范围相当明显,例如:秒和分域的合法值为0到59,小时的合法范围是0到23,Day-of-Month中值得合法凡范围是0到31,但是需要注意不同的月份中的天数不同。月份的合法值是0到11。或者用字符串JAN,FEBMAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV 及DEC来表示。Days-of-Week可以用1到7来表示(1=星期日)或者用字符串SUN, MON, TUE, WED, THU, FRI 和SAT来表示.

'/'字符用来表示值的增量,例如,如果分钟域中放入'0/15',它表示“每隔15分钟,从0开始”,如果在份中域中使用'3/20',则表示“小时中每隔20分钟,从第3分钟开始”或者另外相同的形式就是'3,23,43'。

'?'字符可以用在day-of-month及day-of-week域中,它用来表示“没有指定值”。这对于需要指定一个或者两个域的值而不需要对其他域进行设置来说相当有用。

'L'字符可以在day-of-month及day-of-week中使用,这个字符是"last"的简写,但是在两个域中的意义不同。例如,在day-of-month域中的"L"表示这个月的最后一天,即,一月的31日,非闰年的二月的28日。如果它用在day-of-week中,则表示"7"或者"SAT"。但是如果在day-of-week域中,这个字符跟在别的值后面,则表示"当月的最后的周XXX"。例如:"6L" 或者 "FRIL"都表示本月的最后一个周五。当使用'L'选项时,最重要的是不要指定列表或者值范围,否则会导致混乱。

'W'字符用来指定距离给定日最接近的周几(在day-of-week域中指定)。例如:如果你为day-of-month域指定为"15W",则表示“距离月中15号最近的周几”。

'#'表示表示月中的第几个周几。例如:day-of-week域中的"6#3" 或者 "FRI#3"表示“月中第三个周五”。

作为一个例子,下面的Quartz.NET克隆表达式将在星期一到星期五的每天上午10点15分执行一个作业。

0 15 10 ? * MON-FRI

下面的表达式

0 15 10 ? * 6L 2007-2010

将在2007年到2010年的每个月的最后一个星期五上午10点15分执行作业。你不可能用SimpleTrigger来做这些事情。你可以用两者之中的任何一个,但哪个跟合适则取决于你的调度需要。

清单 2中的 SimpleTrigger 展示了触发器的基础:

清单2 SimpleTriggerRunner.cs

using System;

using System.Collections.Generic;

using System.Text;

using Common.Logging;

using Quartz;

using Quartz.Impl;

namespace QuartzBeginnerExample

{

publicclass SimpleTriggerRunner

{

publicvirtualvoid Run()

{

ILog log = LogManager.GetLogger(typeof(SimpleTriggerExample));

log.Info("-------Initializing -------------------");

// First we must get a reference to ascheduler

ISchedulerFactory sf = new StdSchedulerFactory();

IScheduler sched = sf.GetScheduler();

log.Info("-------Initialization Complete --------");

log.Info("-------Scheduling Jobs ----------------");

// jobs can be scheduled beforesched.start() has been called

// get a "nice round" time a few seconds in the future...

);

// job1 will only fire once at date/time"ts"

JobDetail job = new JobDetail("job1","group1", typeof(SimpleJob));

SimpleTrigger trigger = new SimpleTrigger("trigger1","group1");

// set its start up time

trigger.StartTime = ts;

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

;

// set the number of execution of thisjob, set to 10 times.

// It will run 10 time and exhaust.

;

// schedule it to run!

DateTime ft = sched.ScheduleJob(job, trigger);

log.Info(string.Format("{0} will run at: {1} and repeat: {2} times, every{3} seconds",

)));

log.Info("-------Starting Scheduler ----------------");

// All of the jobs have been added to thescheduler, but none of the jobs

// will run until the scheduler has been started

sched.Start();

log.Info("-------Started Scheduler -----------------");

log.Info("-------Waiting 30 seconds... --------------");

try

{

// wait 30 seconds to show jobs

);

// executing...

}

catch (ThreadInterruptedException)

{

}

log.Info("-------Shutting Down ---------------------");

sched.Shutdown(true);

log.Info("-------Shutdown Complete -----------------");

// display some stats about the schedulethat just ran

SchedulerMetaData metaData = sched.GetMetaData();

log.Info(string.Format("Executed {0} jobs.", metaData.NumJobsExecuted));

}

}

}

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

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

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

清单3 CronTriggerRunner.cs

using System;

using System.Collections.Generic;

using System.Text;

using Common.Logging;

using Quartz;

using Quartz.Impl;

using System.Threading;

namespace QuartzBeginnerExample

{

publicclass CronTriggerRunner

{

publicvirtualvoid Run()

{

ILog log = LogManager.GetLogger(typeof(CronTriggerRunner));

log.Info("-------Initializing -------------------");

// First we must get a reference to ascheduler

ISchedulerFactory sf = new StdSchedulerFactory();

IScheduler sched = sf.GetScheduler();

log.Info("-------Initialization Complete --------");

log.Info("-------Scheduling Jobs ----------------");

// jobs can be scheduled beforesched.start() has been called

// job 1 will run every 20 seconds

JobDetail job = new JobDetail("job1","group1", typeof(SimpleQuartzJob));

CronTrigger trigger = new CronTrigger("trigger1","group1", "job1", "group1");

trigger.CronExpressionString = "0/20* * * * ?";

sched.AddJob(job, true);

DateTime ft = sched.ScheduleJob(trigger);

log.Info(string.Format("{0} has been scheduled to run at: {1} and repeatbased on expression: {2}",job.FullName, ft.ToString("r"), trigger.CronExpressionString));

log.Info("-------Starting Scheduler ----------------");

// All of the jobs have been added to thescheduler, but none of the

// jobs

// will run until the scheduler has been started

sched.Start();

log.Info("-------Started Scheduler -----------------");

log.Info("-------Waiting five minutes... ------------");

try

{

// wait five minutes to show jobs

);

// executing...

}

catch (ThreadInterruptedException)

{

}

log.Info("-------Shutting Down ---------------------");

sched.Shutdown(true);

log.Info("-------Shutdown Complete -----------------");

SchedulerMetaData metaData = sched.GetMetaData();

log.Info(string.Format("Executed {0} jobs.", metaData.NumJobsExecuted));

}

}

}

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

作业管理和存储

作业一旦被调度,调度器需要记住并且跟踪作业和它们的执行次数。如果你的作业是30分钟后或每30秒调用,这不是很有用。事实上,作业执行需要非常准确和即时调用在被调度作业上的Execute()方法。Quartz通过一个称之为作业存储(JobStore)的概念来做作业存储和管理。

有效作业存储

Quartz提供两种基本作业存储类型。第一种类型叫做RAMJobStore,它利用通常的内存来持久化调度程序信息。这种作业存储类型最容易配置、构造和运行。Quartz.net缺省使用的就是RAMJobStore。对许多应用来说,这种作业存储已经足够了。

然而,因为调度程序信息是存储在被分配在内存里面,所以,当应用程序停止运行时,所有调度信息将被丢失。如果你需要在重新启动之间持久化调度信息,则将需要第二种类型的作业存储。为了修正这个问题,Quartz.NET提供了 AdoJobStore。顾名思义,作业仓库通过 ADO.NET把所有数据放在数据库中。数据持久性的代价就是性能降低和复杂性的提高。它将所有的数据通过ADO.NET保存到数据库可中。它的配置要比RAMJobStore稍微复杂,同时速度也没有那么快。但是性能的缺陷不是非常差,尤其是如果你在数据库表的主键上建立索引。

设置AdoJobStore

AdoJobStore几乎可以在任何数据库上工作,它广泛地使用Oracle, MySQL, MS SQLServer2000, HSQLDB, PostreSQL以及 DB2。要使用AdoJobStore,首先必须创建一套Quartz使用的数据库表,可以在Quartz 的database\tables找到创建库表的SQL脚本。如果没有找到你的数据库类型的脚本,那么找到一个已有的,修改成为你数据库所需要的。需要注意的一件事情就是所有Quartz库表名都以QRTZ_作为前缀(例如:表"QRTZ_TRIGGERS",及"QRTZ_JOB_DETAIL")。实际上,可以你可以将前缀设置为任何你想要的前缀,只要你告诉AdoJobStore那个前缀是什么即可(在你的Quartz属性文件中配置)。对于一个数据库中使用多个scheduler实例,那么配置不同的前缀可以创建多套库表,十分有用。

一旦数据库表已经创建,在配置和启动AdoJobStore之前,就需要作出一个更加重要的决策。你要决定在你的应用中需要什么类型的事务。如果不想将scheduling命令绑到其他的事务上,那么你可以通过对JobStore使用JobStoreTX来让Quartz帮你管理事务(这是最普遍的选择)。

最后的疑问就是如何建立获得数据库联接的数据源(DataSource)。Quartz属性中定义数据源是通过提供所有联接数据库的信息,让Quartz自己创建和管理数据源。

要使用AdoJobStore(假定使用StdSchedulerFactory),首先需要设置Quartz配置中的quartz.jobStore.type属性为Quartz.Impl.AdoJobStore.JobStoreTX, Quartz。

配置 Quartz使用 JobStoreTx

quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz

下一步,需要为JobStore选择一个DriverDelegate , DriverDelegate负责做指定数据库的所有ADO.NET工作。StdADO.NETDelegate是一个使用vanilla" ADO.NET代码(以及SQL语句)来完成工作的代理。如果数据库没有其他指定的代理,那么就试用这个代理。只有当使用StdADO.NETDelegate发生问题时,我们才会使用数据库特定的代理(这看起来非常乐观。其他的代理可以在Quartz.Impl.AdoJobStor命名空间找到。)。其他的代理包括PostgreSQLDelegate( 专为PostgreSQL 7.x)。

一旦选择好了代理,就将它的名字设置给AdoJobStore。

配置AdoJobStore使用DriverDelegate

quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz

接下来,需要为JobStore指定所使用的数据库表前缀(前面讨论过)。

配置AdoJobStore的数据库表前缀

quartz.jobStore.tablePrefix = QRTZ

然后需要设置JobStore所使用的数据源。必须在Quartz属性中定义已命名的数据源,比如,我们指定Quartz使用名为"default"的数据源(在配置文件的其他地方定义)。

配置 AdoJobStore使用数据源源的名字

properties["quartz.jobStore.dataSource"] = "default"

最后,需要配置数据源的使用的Ado.net数据提供者和数据库连接串,数据库连接串是标准的Ado.net 数据库连接的连接串。数据库提供者是关系数据库同Quartz.net之间保持低耦合的数据库的连接提供者.

配置AdoJobStore使用数据源源的数据库连接串和数据库提供者

quartz.dataSource.default.connectionString =Server=(local);Database=quartz;Trusted_Connection=True;

quartz.dataSource.default.provider= SqlServer-11

目前Quartz.net支持的以下数据库的数据提供者:

l SqlServer-11 - SQL Server driver for .NET Framework 1.1

l SqlServer-20 - SQL Server driver for .NET Framework 2.0

l OracleClient-20 - Microsoft's Oracle Driver (comes bundled with .NETFramework)

l OracleODP-20 - Oracle's Oracle Driver

l MySql-10 - MySQL Connector/.NET v. 1.0.7

l MySql-109 - MySQL Connector/.NET v. 1.0.9

l MySql-50 - MySQL Connector/.NET v. 5.0 (.NET 2.0)

l MySql-51 - MySQL Connector/:NET v. 5.1 (.NET 2.0)

l SQLite1044 - SQLite ADO.NET 2.0 Provider v. 1.0.44 (.NET 2.0)

如果Scheduler非常忙(比如,执行的任务数量差不多和线程池的数量相同,那么你需要正确地配置DataSource的连接数量为线程池数量。为了指示AdoJobStore所有的JobDataMaps中的值都是字符串,并且能以“名字-值”对的方式存储而不是以复杂对象的序列化形式存储在BLOB字段中,应设置 quartz.jobStore.usePropertiess配置参数的值为"true"(这是缺省的方式)。这样做,从长远来看非常安全,这样避免了对存储在BLOB中的非字符串的序列化对象的类型转换问题。

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

清单4 AdoJobStoreRunner.cs

publicclass AdoJobStoreRunner : IExample

{

publicstring Name

{

get { return GetType().Name; }

}

privatestatic ILog _log = LogManager.GetLogger(typeof(AdoJobStoreRunner));

publicvirtualvoid CleanUp(IScheduler inScheduler)

{

_log.Warn("***** Deletingexisting jobs/triggers *****");

// unschedule jobs

string[] groups = inScheduler.TriggerGroupNames;

; i< groups.Length; i++)

{

String[] names = inScheduler.GetTriggerNames(groups[i]);

; j< names.Length; j++)

inScheduler.UnscheduleJob(names[j], groups[i]);

}

// delete jobs

groups = inScheduler.JobGroupNames;

; i< groups.Length; i++)

{

String[] names = inScheduler.GetJobNames(groups[i]);

; j< names.Length; j++)

inScheduler.DeleteJob(names[j], groups[i]);

}

}

publicvirtualvoid Run(boolinClearJobs,bool inScheduleJobs)

{

NameValueCollection properties = new NameValueCollection();

properties["quartz.scheduler.instanceName"] ="TestScheduler";

properties["quartz.scheduler.instanceId"] ="instance_one";

properties["quartz.threadPool.type"] ="Quartz.Simpl.SimpleThreadPool, Quartz";

properties["quartz.threadPool.threadCount"] ="5";

properties["quartz.threadPool.threadPriority"] ="Normal";

properties["quartz.jobStore.misfireThreshold"] ="60000";

properties["quartz.jobStore.type"] ="Quartz.Impl.AdoJobStore.JobStoreTX, Quartz";

properties["quartz.jobStore.driverDelegateType"] ="Quartz.Impl.AdoJobStore.StdAdoDelegate, Quartz";

properties["quartz.jobStore.useProperties"] ="false";

properties["quartz.jobStore.dataSource"] ="default";

properties["quartz.jobStore.tablePrefix"] ="QRTZ_";

properties["quartz.jobStore.clustered"] ="true";

// if running MS SQL Server we need this

properties["quartz.jobStore.selectWithLockSQL"] ="SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = @lockName";

properties["quartz.dataSource.default.connectionString"] =@"Server=LIJUNNIN-PCSQLEXPRESS;Database=quartz;Trusted_Connection=True;";

properties["quartz.dataSource.default.provider"] ="SqlServer-20";

// First we must get a reference to ascheduler

ISchedulerFactory sf = new StdSchedulerFactory(properties);

IScheduler sched = sf.GetScheduler();

if (inClearJobs)

{

CleanUp(sched);

}

_log.Info("------- InitializationComplete -----------");

if (inScheduleJobs)

{

_log.Info("-------Scheduling Jobs ------------------");

string schedId = sched.SchedulerInstanceId;

;

JobDetail job = new JobDetail("job_" + count, schedId,typeof(SimpleQuartzJob));

// ask scheduler to re-Execute this job ifit was in progress when

// the scheduler went down...

job.RequestsRecovery = true;

, 5000L);

trigger.StartTime = DateTime.Now.AddMilliseconds(1000L);

sched.ScheduleJob(job, trigger);

)));

count++;

job = new JobDetail("job_" + count, schedId,typeof(SimpleQuartzJob));

// ask scheduler to re-Execute this job ifit was in progress when

// the scheduler went down...

job.RequestsRecovery = (true);

, 5000L);

trigger.StartTime = (DateTime.Now.AddMilliseconds(2000L));

sched.ScheduleJob(job, trigger);

)));

count++;

job = new JobDetail("job_" + count, schedId,typeof(SimpleQuartzJob));

// ask scheduler to re-Execute this job ifit was in progress when

// the scheduler went down...

job.RequestsRecovery = (true);

, 3000L);

trigger.StartTime = (DateTime.Now.AddMilliseconds(1000L));

sched.ScheduleJob(job, trigger);

)));

count++;

job = new JobDetail("job_" + count, schedId,typeof(SimpleQuartzJob));

// ask scheduler to re-Execute this job ifit was in progress when

// the scheduler went down...

job.RequestsRecovery = (true);

, 4000L);

trigger.StartTime = (DateTime.Now.AddMilliseconds(1000L));

sched.ScheduleJob(job, trigger);

_log.Info(string.Format("{0} will run at: {1} & repeat: {2}/{3}", job.FullName, trigger.GetNextFireTime(),trigger.RepeatCount, trigger.RepeatInterval));

count++;

job = new JobDetail("job_" + count, schedId,typeof(SimpleQuartzJob));

// ask scheduler to re-Execute this job ifit was in progress when

// the scheduler went down...

job.RequestsRecovery = (true);

, 4500L);

trigger.StartTime = (DateTime.Now.AddMilliseconds(1000L));

sched.ScheduleJob(job, trigger);

_log.Info(string.Format("{0} will run at: {1} & repeat: {2}/{3}", job.FullName, trigger.GetNextFireTime(),trigger.RepeatCount, trigger.RepeatInterval));

}

// jobs don't start firing until start()has been called...

_log.Info("-------Starting Scheduler ---------------");

sched.Start();

_log.Info("-------Started Scheduler ----------------");

_log.Info("-------Waiting for one hour... ----------");

));

_log.Info("-------Shutting Down --------------------");

sched.Shutdown();

_log.Info("-------Shutdown Complete ----------------");

}

publicvoid Run()

{

bool clearJobs =true;

bool scheduleJobs =true;

AdoJobStoreRunner example = new AdoJobStoreRunner();

example.Run(clearJobs, scheduleJobs);

}

}

结束语

Quartz.net作业调度框架所提供的 API 在两方面都表现极佳:既全面强大,又易于使用。Quartz 可以用于简单的作业触发,也可以用于复杂的 Ado.net持久的作业存储和执行。

示例下载 :www.cnblogs.com/Files/shanyou/QuartzBeginnerExample.zip基于 Quartz.net 的示例 (C#代码 ) QuartzBeginnerExample.zip 324KB

获取Quartz.net:quartznet.sourceforge.net/download.html:.NET应用程序的开放源码作业调度解决方案

4.  JAVA任务调度和消息调度

4.1         了解Quartz体系结构

Quartz对任务调度的领域问题进行了高度的抽象,提出了调度器、任务和触发器这3个核心的概念,并在org.quartz通过接口和类对重要的这些核心概念进行描述:

●Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;

●JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

通过该类的构造函数可以更具体地了解它的功用:JobDetail(java.lang.String name, java.lang.String group,java.lang.Class jobClass),该构造函数要求指定Job的实现类,以及任务在Scheduler中的组名和Job名称;

●Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;

●Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。

假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。针对不同时间段类型,Quartz在org.quartz.impl.calendar包下提供了若干个Calendar的实现类,如AnnualCalendar、MonthlyCalendar、WeeklyCalendar分别针对每年、每月和每周进行定义;

●Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。

Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler# getContext()获取对应的SchedulerContext实例;

●ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

Job有一个StatefulJob子接口,代表有状态的任务,该接口是一个没有方法的标签接口,其目的是让Quartz知道任务的类型,以便采用不同的执行方案。无状态任务在执行时拥有自己的JobDataMap拷贝,对JobDataMap的更改不会影响下次的执行。而有状态任务共享共享同一个JobDataMap实例,每次任务执行对JobDataMap所做的更改会保存下来,后面的执行可以看到这个更改,也即每次执行任务后都会对后面的执行发生影响。

正因为这个原因,无状态的Job可以并发执行,而有状态的StatefulJob不能并发执行,这意味着如果前次的StatefulJob还没有执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务需要考虑更多的因素,程序往往拥有更高的复杂度,因此除非必要,应该尽量使用无状态的Job。

如果Quartz使用了数据库持久化任务调度信息,无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存。

Trigger自身也可以拥有一个JobDataMap,其关联的Job可以通过JobExecutionContext#getTrigger().getJobDataMap()获取Trigger中的JobDataMap。不管是有状态还是无状态的任务,在任务执行期间对Trigger的JobDataMap所做的更改都不会进行持久,也即不会对下次的执行产生影响。

Quartz拥有完善的事件和监听体系,大部分组件都拥有事件,如任务执行前事件、任务执行后事件、触发器触发前事件、触发后事件、调度器开始事件、关闭事件等等,可以注册相应的监听器处理感兴趣的事件。

图1描述了Scheduler的内部组件结构,SchedulerContext提供Scheduler全局可见的上下文信息,每一个任务都对应一个JobDataMap,虚线表达的JobDataMap表示对应有状态的任务:

一个Scheduler可以拥有多个Triger组和多个JobDetail组,注册Trigger和JobDetail时,如果不显式指定所属的组,Scheduler将放入到默认组中,默认组的组名为Scheduler.DEFAULT_GROUP。组名和名称组成了对象的全名,同一类型对象的全名不能相同。

Scheduler本身就是一个容器,它维护着Quartz的各种组件并实施调度的规则。Scheduler还拥有一个线程池,线程池为任务提供执行线程——这比执行任务时简单地创建一个新线程要拥有更高的效率,同时通过共享节约资源的占用。通过线程池组件的支持,对于繁忙度高、压力大的任务调度,Quartz将可以提供良好的伸缩性。

提示: Quartz完整下载包examples目录下拥有10多个实例,它们是快速掌握Quartz应用很好的实例。

使用SimpleTrigger

SimpleTrigger拥有多个重载的构造函数,用以在不同场合下构造出对应的实例:

●SimpleTrigger(Stringname, String group):通过该构造函数指定Trigger所属组和名称;

●SimpleTrigger(Stringname, String group, Date startTime):除指定Trigger所属组和名称外,还可以指定触发的开发时间;

●SimpleTrigger(Stringname, String group, Date startTime, Date endTime, int repeatCount, longrepeatInterval):除指定以上信息外,还可以指定结束时间、重复执行次数、时间间隔等参数;

●SimpleTrigger(Stringname, String group, String jobName, String jobGroup, Date startTime, DateendTime, int repeatCount, long repeatInterval):这是最复杂的一个构造函数,在指定触发参数的同时,还通过jobGroup和jobName,让该Trigger和Scheduler中的某个任务关联起来。

通过实现org.quartz..Job接口,可以使 Java 类化身为可调度的任务。代码清单1提供了 Quartz任务的一个示例:

代码清单1 SimpleJob:简单的Job实现类

package com.baobaotao.basic.quartz;

import java.util.Date;

import org.quartz.Job;

importorg.quartz.JobExecutionContext;

importorg.quartz.JobExecutionException;

public class SimpleJobimplements Job {

①实例Job接口方法

public voidexecute(JobExecutionContext jobCtx)throws JobExecutionException {

System.out.println(jobCtx.getTrigger().getName()+" triggered. time is:" + (new Date()));

}

}

这个类用一条非常简单的输出语句实现了Job接口的execute(JobExecutionContextcontext)方法,这个方法可以包含想要执行的任何代码。下面,我们通过SimpleTrigger对SimpleJob进行调度:

代码清单2SimpleTriggerRunner:使用SimpleTrigger进行调度

packagecom.baobaotao.basic.quartz;

import java.util.Date;

importorg.quartz.JobDetail;

importorg.quartz.Scheduler;

importorg.quartz.SchedulerFactory;

importorg.quartz.SimpleTrigger;

importorg.quartz.impl.StdSchedulerFactory;

public classSimpleTriggerRunner {

public static voidmain(String args[]) {

try {

①创建一个JobDetail实例,指定SimpleJob

JobDetail jobDetail =new JobDetail("job1_1","jGroup1", SimpleJob.class);

②通过SimpleTrigger定义调度规则:马上启动,每2秒运行一次,共运行100次

SimpleTriggersimpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");

simpleTrigger.setStartTime(newDate());

simpleTrigger.setRepeatInterval(2000);

simpleTrigger.setRepeatCount(100);

③通过SchedulerFactory获取一个调度器实例

SchedulerFactoryschedulerFactory = new StdSchedulerFactory();

Scheduler scheduler =schedulerFactory.getScheduler();

scheduler.scheduleJob(jobDetail,simpleTrigger);④注册并进行调度

scheduler.start();⑤调度启动

} catch (Exception e) {

e.printStackTrace();

}

}

}

首先在①处通过JobDetail封装SimpleJob,同时指定Job在Scheduler中所属组及名称,这里,组名为jGroup1,而名称为job1_1。

在②处创建一个SimpleTrigger实例,指定该Trigger在Scheduler中所属组及名称。接着设置调度的时间规则。

最后,需要创建Scheduler实例,并将JobDetail和Trigger实例注册到Scheduler中。这里,我们通过StdSchedulerFactory获取一个Scheduler实例,并通过scheduleJob(JobDetailjobDetail, Trigger trigger)完成两件事:

1)将JobDetail和Trigger注册到Scheduler中;

2)将Trigger指派给JobDetail,将两者关联起来。

当Scheduler启动后,Trigger将定期触发并执行SimpleJob的execute(JobExecutionContext jobCtx)方法,然后每 10 秒重复一次,直到任务被执行 100 次后停止。

还可以通过SimpleTrigger的setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定运行的时间范围,当运行次数和时间范围冲突时,超过时间范围的任务运行不被执行。如可以通过simpleTrigger.setStartTime(new Date(System.currentTimeMillis() +60000L))指定60秒钟以后开始。

除了通过scheduleJob(jobDetail,simpleTrigger)建立Trigger和JobDetail的关联,还有另外一种关联Trigger和JobDetail的方式:

JobDetail jobDetail = newJobDetail("job1_1","jGroup1", SimpleJob.class);

SimpleTriggersimpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");

simpleTrigger.setJobGroup("jGroup1");①-1:指定关联的Job组名

simpleTrigger.setJobName("job1_1");①-2:指定关联的Job名称

scheduler.addJob(jobDetail,true);②注册JobDetail

scheduler.scheduleJob(simpleTrigger);③注册指定了关联JobDetail的Trigger

在这种方式中,Trigger通过指定Job所属组及Job名称,然后使用Scheduler的scheduleJob(Triggertrigger)方法注册Trigger。有两个值得注意的地方:

通过这种方式注册的Trigger实例必须已经指定Job组和Job名称,否则调用注册Trigger的方法将抛出异常;

引用的JobDetail对象必须已经存在于Scheduler中。也即,代码中①、②和③的先后顺序不能互换。

在构造Trigger实例时,可以考虑使用org.quartz.TriggerUtils工具类,该工具类不但提供了众多获取特定时间的方法,还拥有众多获取常见Trigger的方法,如makeSecondlyTrigger(StringtrigName)方法将创建一个每秒执行一次的Trigger,而makeWeeklyTrigger(String trigName, int dayOfWeek, int hour, intminute)将创建一个每星期某一特定时间点执行一次的Trigger。而getEvenMinuteDate(Date date)方法将返回某一时间点一分钟以后的时间。

4.2         使用CronTrigger

CronTrigger 能够提供比 SimpleTrigger更有具体实际意义的调度方案,调度规则基于 Cron 表达式,CronTrigger支持日历相关的重复时间间隔(比如每月第一个周一执行),而不是简单的周期时间间隔。因此,相对于SimpleTrigger而言,CronTrigger在使用上也要复杂一些。

4.3         Cron表达式

Quartz使用类似于Linux下的Cron表达式定义时间规则,Cron表达式由6或7个由空格分隔的时间字段组成,如表1所示:

表1 Cron表达式时间字段

位置     时间域名    允许值  允许的特殊字符

1    秒   0-59       , - * /

2    分钟      0-59       ,- * /

3    小时      0-23       ,- * /

4    日期      1-31       ,- * ? / L W C

5    月份      1-12       ,- * /

6    星期      1-7  ,- * ? / L C #

7    年(可选)      空值1970-2099  , - * /

Cron表达式的时间字段除允许设置数值外,还可使用一些特殊的字符,提供列表、范围、通配符等功能,细说如下:

●星号(*):可用在所有字段中,表示对应时间域的每一个时刻,例如,*在分钟字段时,表示“每分钟”;

●问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;

●减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;

●逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;

●斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y;

●L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五;

●W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围;

●LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日;

●井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;

● C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。

Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。

表2下面给出一些完整的Cron表示式的实例:

表2 Cron表示式示例

表示式说明

"0 0 12 * * ?"    每天12点运行

"0 15 10 ? **"   每天10:15运行

"0 15 10 * *?"   每天10:15运行

"0 15 10 * * ?*"       每天10:15运行

"0 15 10 * * ?2008" 在2008年的每天10:15运行

"0 * 14 * * ?"     每天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59。

"0 0/5 14 * *?"  每天14点到15点每5分钟运行一次,开始于14:00,结束于14:55。

"0 0/5 14,18 * *?"    每天14点到15点每5分钟运行一次,此外每天18点到19点每5钟也运行一次。

"0 0-5 14 * *?"  每天14:00点到14:05,每分钟运行一次。

"0 10,44 14 ? 3WED"      3月每周三的14:10分到14:44,每分钟运行一次。

"0 15 10 ? *MON-FRI"    每周一,二,三,四,五的10:15分运行。

"0 15 10 15 *?" 每月15日10:15分运行。

"0 15 10 L *?"    每月最后一天10:15分运行。

"0 15 10 ? *6L"  每月最后一个星期五10:15分运行。

"0 15 10 ? * 6L2007-2009"   在2007,2008,2009年每个月的最后一个星期五的10:15分运行。

"0 15 10 ? *6#3"      每月第三个星期五的10:15分运行。

4.4         CronTrigger实例

下面,我们使用CronTrigger对SimpleJob进行调度,通过Cron表达式制定调度规则,让它每5秒钟运行一次:

代码清单3 CronTriggerRunner:使用CronTrigger进行调度

packagecom.baobaotao.basic.quartz;

importorg.quartz.CronExpression;

importorg.quartz.CronTrigger;

importorg.quartz.JobDetail;

importorg.quartz.Scheduler;

importorg.quartz.SchedulerFactory;

importorg.quartz.impl.StdSchedulerFactory;

public classCronTriggerRunner {

public static voidmain(String args[]) {

try {

JobDetail jobDetail =new JobDetail("job1_2", "jGroup1",SimpleJob.class);

①-1:创建CronTrigger,指定组及名称

CronTrigger cronTrigger= new CronTrigger("trigger1_2", "tgroup1");

CronExpression cexp =new CronExpression("0/5 * * * * ?");①-2:定义Cron表达式

cronTrigger.setCronExpression(cexp);①-3:设置Cron表达式

SchedulerFactoryschedulerFactory = new StdSchedulerFactory();

Scheduler scheduler =schedulerFactory.getScheduler();

scheduler.scheduleJob(jobDetail,cronTrigger);

scheduler.start();

//②

} catch (Exception e) {

e.printStackTrace();

}

}

}

运行CronTriggerRunner,每5秒钟将触发运行SimpleJob一次。默认情况下Cron表达式对应当前的时区,可以通过CronTriggerRunner的setTimeZone(java.util.TimeZone timeZone)方法显式指定时区。你还也可以通过setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定开始和结束的时间。

在代码清单3的②处需要通过Thread.currentThread.sleep()的方式让主线程睡眠,以便调度器可以继续工作执行任务调度。否则在调度器启动后,因为主线程马上退出,也将同时引起调度器关闭,调度器中的任务都将相应销毁,这将导致看不到实际的运行效果。在单元测试的时候,让主线程睡眠经常使用的办法。对于某些长周期任务调度的测试,你可以简单地调整操作系统时间进行模拟。

4.5         使用Calendar

在实际任务调度中,我们不可能一成不变地按照某个周期性的调度规则运行任务,必须考虑到实现生活中日历上特定日期,就象习惯了大男人作风的人在2月14号也会有不同表现一样。

下面,我们安排一个任务,每小时运行一次,并将五一节和国际节排除在外,其代码如代码清单4所示:

代码清单4CalendarExample:使用Calendar

packagecom.baobaotao.basic.quartz;

importjava.util.Calendar;

import java.util.Date;

importjava.util.GregorianCalendar;

importorg.quartz.impl.calendar.AnnualCalendar;

importorg.quartz.TriggerUtils;

public classCalendarExample {

public static voidmain(String[] args) throws Exception {

SchedulerFactory sf =new StdSchedulerFactory();

Scheduler scheduler =sf.getScheduler();

①法定节日是以每年为周期的,所以使用AnnualCalendar

AnnualCalendar holidays= new AnnualCalendar();

②五一劳动节

Calendar laborDay = newGregorianCalendar();

laborDay.add(Calendar.MONTH,5);

laborDay.add(Calendar.DATE,1);

holidays.setDayExcluded(laborDay,true);②-1:排除的日期,如果设置为false则为包含

③国庆节

Calendar nationalDay =new GregorianCalendar();

nationalDay.add(Calendar.MONTH,10);

nationalDay.add(Calendar.DATE,1);

holidays.setDayExcluded(nationalDay,true);③-1:排除该日期

scheduler.addCalendar("holidays",holidays, false, false);④向Scheduler注册日历

Date runDate =TriggerUtils.getDateOf(0,0, 10, 1, 4);⑤4月1号上午10点

JobDetail job = newJobDetail("job1", "group1", SimpleJob.class);

SimpleTrigger trigger =new SimpleTrigger("trigger1", "group1",

runDate,

null,

SimpleTrigger.REPEAT_INDEFINITELY,

60L * 60L * 1000L);

trigger.setCalendarName("holidays");⑥让Trigger应用指定的日历规则

scheduler.scheduleJob(job,trigger);

scheduler.start();

//实际应用中主线程不能停止,否则Scheduler得不到执行,此处从略

}

}

由于节日是每年重复的,所以使用org.quartz.Calendar的AnnualCalendar实现类,通过②、③的代码,指定五一和国庆两个节日并通过AnnualCalendar#setDayExcluded(Calendar day, boolean exclude)方法添加这两个日期。exclude为true时表示排除指定的日期,如果为false时表示包含指定的日期。

在定制好org.quartz.Calendar后,还需要通过Scheduler#addCalendar(String calName, Calendar calendar, booleanreplace, boolean updateTriggers)进行注册,如果updateTriggers为true,Scheduler中已引用Calendar的Trigger将得到更新,如④所示。

在⑥处,我们让一个Trigger指定使用Scheduler中代表节日的Calendar,这样Trigger就会避开五一和国庆这两个特殊日子了。

任务调度信息存储

在默认情况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。

比如我们希望安排一个执行100次的任务,如果执行到50次时系统崩溃了,系统重启时任务的执行计数器将从0开始。在大多数实际的应用中,我们往往并不需要保存任务调度的现场数据,因为很少需要规划一个指定执行次数的任务。

对于仅执行一次的任务来说,其执行条件信息本身应该是已经持久化的业务数据(如锁定到期解锁任务,解锁的时间应该是业务数据),当执行完成后,条件信息也会相应改变。当然调度现场信息不仅仅是记录运行次数,还包括调度规则、JobDataMap中的数据等等。

如果确实需要持久化任务调度信息,Quartz允许你通过调整其属性文件,将这些信息保存到数据库中。使用数据库保存任务调度信息后,即使系统崩溃后重新启动,任务的调度信息将得到恢复。如前面所说的例子,执行50次崩溃后重新运行,计数器将从51开始计数。使用了数据库保存信息的任务称为持久化任务。

4.6         通过配置文件调整任务调度信息的保存策略

其实Quartz JAR文件的org.quartz包下就包含了一个quartz.properties属性配置文件并提供了默认设置。如果需要调整默认配置,可以在类路径下建立一个新的quartz.properties,它将自动被Quartz加载并覆盖默认的设置。

先来了解一下Quartz的默认属性配置文件:

代码清单5quartz.properties:默认配置

①集群的配置,这里不使用集群

org.quartz.scheduler.instanceName= DefaultQuartzScheduler

org.quartz.scheduler.rmi.export= false

org.quartz.scheduler.rmi.proxy= false

org.quartz.scheduler.wrapJobExecutionInUserTransaction= false

②配置调度器的线程池

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

org.quartz.threadPool.threadCount= 10

org.quartz.threadPool.threadPriority= 5

org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread= true

③配置任务调度现场数据保存机制

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

Quartz的属性配置文件主要包括三方面的信息:

1)集群信息;

2)调度器线程池;

3)任务调度现场数据的保存。

如果任务数目很大时,可以通过增大线程池的大小得到更好的性能。默认情况下,Quartz采用org.quartz.simpl.RAMJobStore保存任务的现场数据,顾名思义,信息保存在RAM内存中,我们可以通过以下设置将任务调度现场数据保存到数据库中:

代码清单6quartz.properties:使用数据库保存任务调度现场数据

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

org.quartz.jobStore.tablePrefix= QRTZ_①数据表前缀

org.quartz.jobStore.dataSource= qzDS②数据源名称

③定义数据源的具体属性

org.quartz.dataSource.qzDS.driver= oracle.jdbc.driver.OracleDriver

org.quartz.dataSource.qzDS.URL= jdbc:oracle:thin:@localhost:1521:ora9i

org.quartz.dataSource.qzDS.user= stamen

org.quartz.dataSource.qzDS.password= abc

org.quartz.dataSource.qzDS.maxConnections= 10

要将任务调度数据保存到数据库中,就必须使用org.quartz.impl.jdbcjobstore.JobStoreTX代替原来的org.quartz.simpl.RAMJobStore并提供相应的数据库配置信息。首先①处指定了Quartz数据库表的前缀,在②处定义了一个数据源,在③处具体定义这个数据源的连接信息。

你必须事先在相应的数据库中创建Quartz的数据表(共8张),在Quartz的完整发布包的docs/dbTables目录下拥有对应不同数据库的SQL脚本。

4.7         查询数据库中的运行信息

任务的现场保存对于上层的Quartz程序来说是完全透明的,我们在src目录下编写一个如代码清单6所示的quartz.properties文件后,重新运行代码清单2或代码清单3的程序,在数据库表中将可以看到对应的持久化信息。当调度程序运行过程中途停止后,任务调度的现场数据将记录在数据表中,在系统重启时就可以在此基础上继续进行任务的调度。

代码清单7JDBCJobStoreRunner:从数据库中恢复任务的调度

package com.baobaotao.basic.quartz;

importorg.quartz.Scheduler;

importorg.quartz.SchedulerFactory;

importorg.quartz.SimpleTrigger;

importorg.quartz.Trigger;

importorg.quartz.impl.StdSchedulerFactory;

public classJDBCJobStoreRunner {

public static voidmain(String args[]) {

try {

SchedulerFactoryschedulerFactory = new StdSchedulerFactory();

Scheduler scheduler =schedulerFactory.getScheduler();

①获取调度器中所有的触发器组

String[] triggerGroups =scheduler.getTriggerGroupNames();

②重新恢复在tgroup1组中,名为trigger1_1触发器的运行

for (int i = 0; i <triggerGroups.length; i++) {

String[] triggers =scheduler.getTriggerNames(triggerGroups[i]);

for (int j = 0; j <triggers.length; j++) {

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

if (tg instanceofSimpleTrigger

&& tg.getFullName().equals("tgroup1.trigger1_1")){②-1:根据名称判断

②-1:恢复运行

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

}

}

}

scheduler.start();

} catch (Exception e) {

e.printStackTrace();

}

}

}

当代码清单2中的SimpleTriggerRunner执行到一段时间后非正常退出,我们就可以通过这个JDBCJobStoreRunner根据记录在数据库中的现场数据恢复任务的调度。Scheduler中的所有Trigger以及JobDetail的运行信息都会保存在数据库中,这里我们仅恢复tgroup1组中名称为trigger1_1的触发器,这可以通过如②-1所示的代码进行过滤,触发器的采用GROUP.TRIGGER_NAME的全名格式。通过Scheduler#rescheduleJob(String triggerName,String groupName,TriggernewTrigger)即可重新调度关联某个Trigger的任务。

下面我们来观察一下不同时期qrtz_simple_triggers表的数据:

1.运行代码清单2的SimpleTriggerRunner一小段时间后退出:

REPEAT_COUNT表示需要运行的总次数,而TIMES_TRIGGER表示已经运行的次数。

2.运行代码清单7的JDBCJobStoreRunner恢复trigger1_1的触发器,运行一段时间后退出,这时qrtz_simple_triggers中的数据如下:

首先Quartz会将原REPEAT_COUNT-TIMES_TRIGGER得到新的REPEAT_COUNT值,并记录已经运行的次数(重新从0开始计算)。

3.重新启动JDBCJobStoreRunner运行后,数据又将发生相应的变化:

4.继续运行直至完成所有剩余的次数,再次查询qrtz_simple_triggers表:

这时,该表中的记录已经变空。

值得注意的是,如果你使用JDBC保存任务调度数据时,当你运行代码清单2的SimpleTriggerRunner然后退出,当再次希望运行SimpleTriggerRunner时,系统将抛出JobDetail重名的异常:

Unable to store Job withname: 'job1_1' and group: 'jGroup1', because one already exists with thisidentification.

因为每次调用Scheduler#scheduleJob()时,Quartz都会将JobDetail和Trigger的信息保存到数据库中,如果数据表中已经同名的JobDetail或Trigger,异常就产生了。

本文使用quartz 1.6版本,我们发现当后台数据库使用MySql时,数据保存不成功,该错误是Quartz的一个Bug,相信会在高版本中得到修复。因为HSQLDB不支持SELECT * FROM TABLE_NAME FOR UPDATE的语法,所以不能使用HSQLDB数据库。

5.  总结

一句话总结:

概念:定时或定期执行任务的一套框架。

1)分任务,调度器,触发器。

2)任务,触发器是多对多关系。

3)可以保存任务到数据库。

代码