Quartz Job Scheduling Framework[翻译]第四章. 部署 Job (第一部分)

第四章. 部署 Job

在上一章中,你首次尝试使用了 Quartz 来部署 Job。无可否认地,那些 Job 都不是很复杂,但这个不是重点。你应该轻松的对如何构造并部署 Job 有了相当的了解,更重要的是,由此热情的希望学得更多的东西。在本章中将会继续给你讲述。

第四章将带领你深入到 Quart 框架的核心部分。可证明的是,这一章对于阅读和理解本书是非常之重要的。调度器(Scheduler) 是此框架的心脏。本章关注于如何使用 Scheduler 来管理你的 Job;如何创建并关联触发器以使 Job 能被触发执行;以及如可选择 calendar 为给定的时程安排提供更多的灵活性。

此刻,什么也没发生,下一刻,仍旧什么也没发生。
道格拉斯.亚当斯,《银河系漫游指南》

1. Quartz 调度器

Quartz 框架包含许多的类和接口,它们分布在大概 11 个包中。多数你所要使用到的类或接口放置在 org.quartz 包中。这个包含盖了 Quartz 框架的公有 API.

我们不打算对这个框架的所有类和接口都面面俱到。而所要介绍的是那些有助于你理解 Quartz 如何做它该做事情的组件的子集。图 4.1 展示了一个只留下必要的调度器的精简类图。

图 4.1. Quartz 类图(仅显示主要组件)
QuartzFigure4.1.JPG

Scheduler 是 Quartz 的主要 API。对于 Quartz 用户来说,多数时候与框架的交互是发生于 Scheduler  之上的。客服端与 Scheduler 交互是通过 org.quartz.Scheduler 接口的。这个 Scheduler 的实现,在这种情况下,是一个代理,对其中方法调用会传递到 QuartzScheduler 实例上。QuartzScheduler 对于客户端是不可见的,并且也不存在与此实例的直接交互。

QuartzScheduler 处在框架根的位置,它是一个引擎驱动着整个框架。并非所有的功能都直接内建到 QuartzScheduler,然而,框架为灵活性和可配置性考虑而设计,所以许多重要的功能由分离的组件和子框架实现。这就意味着 Quartz 用户可以用自己某个关键特征实现来替换原有默认实现。即使 QuartzScheduler 代理了它的一些职责,但它仍然掌控着整个作业调度流程。

·Quartz Scheduler 类层次

客户端会同两种类型的 Scheduler 交互,如图 4.2. 它们都实现了 org.quartz.Scheduler 接口。

图 4.2. org.quartz.Scheduler 类层次
QuartzFigure4.2.JPG

作为一个 Quartz 用户,你要与实现了 org.quartz.Scheduler 接口的类交互。在你调用它的任何 API 之前,你需要知道如何创建一个 Scheduler 的实例。

2. Quartz SchedulerFactory

尽管你已使用到了 Scheduler 类型了,但你未曾显式的去创建 Scheduler 的实例。取而代之的是用了某个工厂方法来确保了构造出 Sheduler 实例并正确的得到初始化。(工厂设计模式之所以谓之工厂模式是因为它承担了生产对象的职责。在这里是生产了一个 Scheduler 实例) Quartz 框架为这一目的提供了 org.quartz.SchedulerFactory 接口。角色 SchedulerFactory 就是用来产生 Scheduler 实例的。当 Scheduler 实例被创建之后,就会存到一个仓库中(org.quartz.impl.SchedulerRepository),这个仓库还提供了通过一个 class loader 查询实例的机制。要使用 Scheduler 实例,客户端必须从工厂(和随同的仓库中)使用不同方法调用来获取到它们。换句话说,要通过工厂创建一个 Scheduler 实例并获取到它需要经由两次方法调用。有一些方便的方法封装了那两个方法,你将很快能看到。

你可使用两种不同类型的 SchedulerFactory 来创建 Scheduler 实例 (看图 4.3)

图 4.3. 所有的 Scheduler 实例应该由 SchedulerFactory 来创建
QuartzFigure4.3.JPG

这两个 Scheduler 工厂分别是 org.quartz.impl.DirectoSchedulerFactoryorg.quartz.impl.StdSchedulerFactory. 让我们来逐个检视它们。

·使用 DirectSchedulerFactory

DirectSchedulerFactory 是为那些想绝对控制 Scheduler 实例是如何生产出的人所设计的。代码 4.1 显示了最简单的方式去使用 DirectSchedulerFactory 来创建一个 Scheduler 实例。

代码 4.1. 使用

DirectSchedulerFactory

当使用 DirectSchedulerFactory 时,有三个基本的步骤。首先,你必须用静态方法 getInstance() 获取到工厂的实例。当你持有了工厂的实例之后,你必须调用其中的一个 createXXX 方法去初始化它。如代码 4.1 所示例子中是调用 createVolatileScheduler() 方法告诉工厂以十个工作者线程初始化它自己(至于工作者线程的更多内容将在本章的后部分讨论到)。第三步也就是最后一步是通过工厂的 getScheduler() 方法拿到 Scheduler 的实例。

在调用 getScheduler() 方法之后调用其中的一个 createXXX 方法
方法 createVolatileScheduler() 方法不会返回 scheduler 的实例。createXXX() 方法是告诉工厂如何配置要创建的 Scheduler 实例。你必须调用方法 getScheduler() 获取到在工厂上执行方法 createXXX() 产生的实例。实际上,在调用 getScheduler() 方法之前,你必须调用其中一个 createXXX() 方法;否则,你将有收到一个 SchedulerException 错误,因为根本没有 Scheduler 实例存在。

你可从数个不同的 createXXX() 方法中选择,依赖于你想要的 Scheduler 类型和你需要怎样的配置。代码 4.1 中用的是 createVolatileScheduler() 方法创建 Scheduler 实例的。方法 createVolatileScheduler() 带有单个参数:要创建的线程数量。在图 4.2 中,你已看到还有一个 RemoteScheduler 类。你必须要用一个不同的 createXXX() 方法去创建 RemoteScheduler 实例。有两个版本的方法可用:

RemoteScheduler 会在第十章,“J2EE 中使用 Quartz”讨论。假如你就是想要一个标准的 Scheduler, 可以调用以下三个版本的方法中的一个:

在上一章上,我们用了一个属性文件来初始化 Scheduler。要通过 DirectSchedulerFactory 创建一个 Scheduler 实例,你必须传递配置参数给其中的一个 createXXX() 方法。在下一节中,我们讨论 StdSchedulerFactory,一个 SchedulerFactory 版本,它依赖于一系列的属性来配置 Scheduler,而不是通过 createXXX() 方法参数来传递配置参数。这样也避免了在代码中对 Scheduler 的配置选项的硬编码。

·使用 StdSchedulerFactory

DirectSchedulerFactory 形成鲜明对比的是,org.quartz.impl.StdSchedulerFactory 依赖于一系列的属性来决定如何生产出 Scheduler 实例。你可以通过以下三种途径向工厂提供那些属性:

    ·通过 java.util.Properties 实例提供

    ·通过外部属性文件提供

    ·通过含用属性文件内容的 java.io.InputStream 实例提供

Java 属性文件
我们这里使用述语“属性文件”,对于 Java 传统来说就是:在一个外部文件中指定一系列的 key=value  对,并且每个 key=value 对独占一行。

代码 4.2 演示了第一种途径,通过一个 java.util.Properties 实例来提供属性。

代码 4.2. 使用 java.util.Properties 实例创建 StdSchedulerFactory

代码 4.2 提供了一个使用 StdSchedulerFactory 创建 Scheduler 实例的很简单的例子。在这个例子中向工厂传递了两个属性,它们分别是实现了 org.quartz.spi.ThreadPool 接口的类名和 Scheduler 用来处理 Job 的线程的数量。这两个属性是必须的,因为它们并没有被指定默认值(我们很快在后面讨论它们)

在代码 4.1 中的 DirectSchedulerFactory,我们调用它其中一个 createXXX() 方法来初始化工厂。而对于 StdSchedulerFactory, 你使用的是一个有效的 initialize() 方法。在工厂初始化之后,你就可以调用它的 getScheduler() 方法获取到 Scheduler 的实例。使用 java.util.Properties 对象传递属性给工厂是一种配置 SchedulerFactory 的方式之一。硬编码配置属性的做法基本不推荐,也是可能的时候应尽力避免的做法。假如你需要修改上一例子中的线程数量,你将不得不修改代码然后重新编译。

幸运的是,StdSchedulerFactory 还有其他的方式来提供必须的属性。工厂也能通过传入一个外部文件名而被初始化,在这个外部文件中包含了这些配置项。应使用 initialize() 的替代方法形式如下:

要使文件和属性能被成功加载的话,这个文件必须对于 classloader 是可见的。也就是说它必须在你的应用程序的 classpath 中。假如你用的是 java.io.InputStream 去加载文件,你可以使用另一个 initialize() 的替代方法如下:

在第三章,“Hello, Quartz” 你已看到为 SchedulerFactory 从一个叫做 quartz.properties 的外部文件中加载设置的例子。这个外部属性文件就是要用前面的方法来加载。假如你没有为 initialize() 方法指定从哪儿读取属性,那么 StdSchedulerFactory 会试图从名为 quartz.properties 的文件中加载它们。这就是你在第三章看到的行为。

·使用默认的 quartz.properties 文件创建 Scheduler

假如你使用无参的 initialize() 方法,StdSchedulerFactory 会执行以下几个步骤去尝试为工厂加载属性:

1.  检查 System.getProperty("org.quartz.properties") 中是否设置了别的文件名

2.  否则,使用 quartz.properties 作为要加载的文件名

3.  试图从当前工作目录中加载这个文件

4.  试图从系统 classpath 下加载这个文件

在 Quartz Jar 包中的默认 quartz.properties 文件

上面第4步总是能成功的,因为在 Quartz Jar 包中有一个默认的 quartz.properties 文件。假如你想使用另一个替代文件,你必须自己创建一个并确保它在 classpath 上。

使用 StdSchedulerFactory 来创建 Scheduler 实例的方式很普遍,因此在 StdSchedulerFactory 直接提供了一个方便的静态方法 getDefaultScheduler(),它就是使用前面列出的几个步骤来初始化工厂的。这如代码 4.3 所示。

代码 4.3. 使用静态的 getDefaultScheduler() 方法创建 Scheduler

在静态方法 getDefaultScheduler() 方法中调用了空的构造方法。假如之前未调用过任何一个 initialize() 方法,那么无参的 initialize() 方法会被调用。这会开始去按照前面说的顺序加载文件。默认情况下,quartz.properties 会被定位到,并从中加载属性。

·Scheduler 的功能

本章到此为止大部分笔墨都在着重论述如何获得一个 Scheduler 的实例。那么一旦你拿到 Scheduler 的实例之后,你能能此做些什么呢?好,现在开始,上面的例子中告诉了你可以调用它的 start() 方法。Scheduler 的 API 大概包括了 65 个不同的方法。我们不在此全部枚举出来,但是你需要理解其中的一小部分 API。

Scheduler 的 API 可以分组成以下三个类别:

    ·管理 Scheduler

    ·管理 Job

    ·管理 Trigger 和 Calendar

本文链接 https://yanbin.blog/quartz-job-scheduling-framework-4-1/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

27 Comments
Inline Feedbacks
View all comments
paul
paul
16 years ago

建议老兄 翻译完了 弄个pdf/chm的下载!方便学习者

隔叶黄莺
16 years ago

谢谢您的建议,只是工作一直都很忙的,没法一个心的做,只能先连载,等完了之后再集个册子供大家点评

坚持学习,每天进步一些

辛苦了楼主,期待下面的,也建议弄个PDF/CHM的供下学习

卓凡
卓凡
15 years ago

请教,为什么我用quartz的时候同事并发了2个线程,也就是说job执行了2次?

隔叶黄莺
15 years ago

quartz 有多个工作线程,不只并发2个线程

job 执行了2次,要看你是否部署过两次?两个 Trigger 关联到同一个 Job? 间隔时间太短,前面没执行完,后面的就启动了。

卓凡
卓凡
15 years ago

@隔叶黄莺
org.quartz.threadPool.threadCount = 1
我这样设置1个线程没用

卓凡
卓凡
15 years ago

<quartz>
<job>
<job-detail>
<name>test</name>
<group>DEFAULT</group>
<description>testJobhere</description>
<job-class>com.d9fans.schedule.MailJob</job-class>
<job-data-map allows-transient-data="true">
<entry>
<key>name</key>
<value>test</value>
</entry>
</job-data-map>
</job-detail>
<trigger>
<cron>
<name>testCron</name>
<group>DEFAULT</group>
<job-name>test</job-name>
<job-group>DEFALUT</job-group>
<cron-expression>0 0/10 * * * ?</cron-expression>
</cron>
</trigger>
</job>
<job>
<job-detail>
<name>updateStatus</name>
<group>DEFAULT2</group>
<description>a job that update status</description> <job-class>com.xue24.schedule.UpdateStatusJob</job-class>
</job-detail>
<trigger>
<cron>
<name>updateStatusCron</name>
<group>DEFAULT2</group>
<job-name>updateStatus</job-name>
<job-group>DEFAULT2</job-group>
<cron-expression>0 0/2 * * * ?</cron-expression>
</cron>
</trigger>
</job>
<job>
<job-detail>
<name>CheckAndUpdateStatusJob</name>
<group>DEFAULT3</group>
<description>a job that transform status</description> <job-class>com.xue24.schedule.CheckAndUpdateStatusJob</job-class>
</job-detail>
<trigger>
<cron>
<name>CheckAndUpdateStatusJob</name>
<group>DEFAULT3</group>
<job-name>CheckAndUpdateStatusJob</job-name>
<job-group>DEFAULT3</job-group>
<cron-expression>0 7 9 * * ?</cron-expression>
</cron>
</trigger>
</job>
</quartz>
我Job这样的,都是很简单的配置

隔叶黄莺
15 years ago

你设置org.quartz.threadPool.threadCount = 1

执行多个 Job 时就会出问题,造成 Job 不能在正确的时间执行。

卓凡
卓凡
15 years ago

@隔叶黄莺没有,现在job是在正确的时间执行了,就是每一个job执行了两次,日志这样的:[13:40:00.092][INFO] LoggingJobHistoryPlugin - -Job DEFAULT.test fired (by trigger DEFAULT.testCron) at: 13:40:00 06/20/2008[13:40:00.094][INFO] LoggingJobHistoryPlugin - -Job DEFAULT2.updateStatus fired (by trigger DEFAULT2.updateStatusCron) at: 13:40:00 06/20/2008[13:40:00.095][INFO] LoggingJobHistoryPlugin - -Job DEFAULT.test fired (by trigger DEFAULT.testCron) at: 13:40:00 06/20/2008[13:40:00.099][INFO] LoggingJobHistoryPlugin - -Job DEFAULT2.updateStatus fired (by trigger DEFAULT2.updateStatusCron) at: 13:40:00 06/20/2008[13:40:00.105][INFO] LoggingJobHistoryPlugin - -Job DEFAULT3.CheckAndUpdateStatusJob fired (by trigger DEFAULT3.CheckAndUpdateStatusJob) at: 13:40:00 06/20/2008[13:40:00.112][INFO] LoggingJobHistoryPlugin - -Job DEFAULT3.CheckAndUpdateStatusJob fired (by trigger DEFAULT3.CheckAndUpdateStatusJob) at: 13:40:00 06/20/2008[13:40:00.450][INFO] LoggingJobHistoryPlugin - -Job DEFAULT2.updateStatus execution complete at 13:40:00 06/20/2008 and reports: null[13:40:00.484][INFO] LoggingJobHistoryPlugin - -Job DEFAULT2.updateStatus execution complete at 13:40:00 06/20/2008 and reports: null[13:40:00.861][INFO] LoggingJobHistoryPlugin - -Job DEFAULT3.CheckAndUpdateStatusJob execution complete at 13:40:00 06/20/2008… Read more »

隔叶黄莺
15 years ago

检查一下有没有启动两个 Scheduler,

适当的把工作线程调大一些。

卓凡
卓凡
15 years ago

@隔叶黄莺我看日志间隔10多秒启动了两个scheduler:[14:33:05.539][INFO] QuartzScheduler - -Scheduler QuartzScheduler_$_NON_CLUSTERED started.[14:33:12.316][INFO] PropertyMessageResources - -Initializing, config='org.apache.struts.util.LocalStrings', returnNull=true[14:33:12.317][INFO] PropertyMessageResources - -Initializing, config='org.apache.struts.action.ActionResources', returnNull=true[14:33:14.060][INFO] PropertyMessageResources - -Initializing, config='com.portal.struts.ApplicationResources', returnNull=true[14:33:14.432][INFO] ServletToolboxManager - -Using config file '/WEB-INF/toolbox.xml'[14:33:15.406]log4j:ERROR Could not find value for key log4j.appender.R.layout[14:33:16.111][INFO] PropertyMessageResources - -Initializing, config='org.apache.struts.util.LocalStrings', returnNull=true[14:33:16.112][INFO] PropertyMessageResources - -Initializing, config='org.apache.struts.action.ActionResources', returnNull=true[14:33:17.107][INFO] PropertyMessageResources - -Initializing, config='ApplicationResources', returnNull=true[14:33:17.180][INFO] PropertyMessageResources - -Initializing, config='com.agent.struts.ApplicationResources', returnNull=true[14:33:17.445][INFO] JobInitializationPlugin - -Registering Quartz Job Initialization Plug-in.[14:33:17.447][INFO] RAMJobStore - -RAMJobStore initialized.[14:33:17.447][INFO] StdSchedulerFactory - -Quartz scheduler 'QuartzScheduler' initialized from the specified file : 'quartz.properties' from the class resource path.[14:33:17.447][INFO] StdSchedulerFactory - -Quartz scheduler version: 1.4.5[14:33:17.486][INFO] LoggingJobHistoryPlugin - -Job JobInitializationPlugin.JobInitializationPlugin_jobInitializer fired (by trigger JobInitializationPlugin.JobInitializationPlugin_jobInitializer) at: 14:33:17 06/20/2008[14:33:17.494][INFO] LoggingJobHistoryPlugin… Read more »

隔叶黄莺
15 years ago

发现问题就好办了,检查一下程序为什么会启动两个 Scheduler。

卓凡
卓凡
15 years ago

@隔叶黄莺
我还没找到原因,scheduler都是采用声明的方式来进行的
会不会和应用服务器有关系?

隔叶黄莺
15 years ago

如果你是在应用服务器通过 Web 应用来启动 Scheduler,那么最好不要在集群环境中使用 Quartz,否则会在每一个节点启动一个 Scheduler。

虽然 Quartz 支持集群环境的部署,能避免一个 Job 被同时执行多次,但个人认为 Quartz 在这方面的实现还不尽完美。

把 Quartz 应用部署到非集群的应用服务器上或者以控制台程序单独启用,然后用 RMI 远程方式来管理。

不知道楼上的朋友是不是这种情形?

卓凡
卓凡
15 years ago

@隔叶黄莺
恩很奇怪,这原因考虑过,我这边应用服务器没有集群环境。

隔叶黄莺
15 years ago

那这样,设个断点,看调用栈,看在哪里启动的 Scheduler.

卓凡
卓凡
15 years ago

@隔叶黄莺
我是声明的方式来启动scheduler,没有编码来启动。

隔叶黄莺
15 years ago

在 Scheduler 的 start() 方法上设断点,不管是声明还是编码方式都能走到这里来的。

Jason
Jason
15 years ago

你好!我有个问题想请教一下高手们,问题就是:我现在一次执行多个Job,但是有个条件,就是在执行某一个job的时候,要先查看此job上一次有没有执行完,如果上一次此job没有执行结束,那么此job将放弃此次执行。同时我要将每次执行的时间做一次统计,job在放弃的同时不记录执行时间。我该如何处理呢?我是在job监听里面做处理呢?还是在job执行的方法里面进行处理呢?由于每个job使用同一个监听,其中有些job没有放弃执行的需求。紧急!谢谢 高手们!

隔叶黄莺
15 years ago

1. 如果你在 properties 文件中只设置一个工作者线程,那么可以通过设置

org.quartz.jobStore.misfireThreshold 为很小的值来使前一个 Job 没执行完,后续的 Job 直接丢弃,Job 中统计时间就行

2. 或者在 Job 中锁定一个有限资源,如端口/文件/静态容器中的元素/数据库表记录 来让同时只能一个同名 Job 执行,得不到有限资源时,Job 方法中直接 return,要保证 Job 结束时释放掉所占据的资源。同样只要在 Job 中统计时间

你也可以考虑用监听器,但监听器通常被用作为一种异步的方式,对于已启动的 Job 要能够适时终止。

Jason
Jason
15 years ago

你好!不好意思! 我才是刚刚接触quartz,看的不是很明白!但是我现在根据项目的实际要求只能选择对job的监听器进行控制,但是我job监听器当中如何终止job的运行还是不明白,能说的详细一点吗?谢谢高手!我的msn:wangyi669@hotmail.com 请你有机会加我!谢谢

隔叶黄莺
15 years ago

你也叫 Jason,我们楼下七楼机房也有一个叫 Jason 的,关于监听器的更具体的用法,我也得好好看看 API。

隔叶黄莺
15 years ago

关于在 TriggerListener 中如何终止本次 Job 的执行请参看我翻译的这篇日志:

http://www.blogjava.net/Unmi/archive/2008/07/06/212816.html

在 TriggerListener 的方法 vetoJobExecution() 中返回 true,就意味着否绝本次 Job(实际为 Trigger) 的执行,因为这个监听方法是在 Job 的 execute() 方法这前执行的。

jason
jason
15 years ago

谢谢了!我所使用quartz中没有使用triggerlistener,不过现在问题基本解决了,我是直接在每个job中设置静态变量来区分是否执行结束!

jason
jason
15 years ago

请问一下,org.quartz.jobStore.misfireThreshold 是做什么用的,它后面一般情况下9Job应该设置什么样的数字。

隔叶黄莺
15 years ago

关于 org.quartz.jobStore.misfireThreshold 是用途可以参考我之前对这个问题的回复:

http://www.blogjava.net/Unmi/archive/2007/10/17/153413.html#155390

这其中有针对 misfireThreshold 的一个试验,通过这个试验应该可以很好的理解 misfireThreshold 的作用

9 个 job 时,这个数字应为多少,还是要根据你的 job 的执行周期和每次执行时长来定,如果能够把任务全错开就可以不管这个值。

james
james
15 years ago

兄弟的翻译确实不错,受益非浅呀,还望二天能整理成PDF格式提供下载就好了,我就能好生收藏了.