Quartz 项目应用笔记

来源:互联网 发布:未加工数据跑统计模型 编辑:程序博客网 时间:2024/05/16 11:21

Quartz 项目应用笔记

Quartz 是一个强大的企业级 Schedule 工具,也是目前最好的开源 Schedule 工具,最近因为项目的需要,简单的用到了 Quartz 的一些功能,对项目中使用 Quartz 的一些问题做简单的记录。

在 Quartz 的应用中,我们用到了以下的一些东西,ScheduleFactory, Scheduler, Job, JobDetail, Trigger,简单说明一下他们的用途。

SchedulerFactory 是 Scheduler 的工厂,我们可以从中获得受工厂管理的 Scheduler 对象。

SchedulerFactory scheduleFactory = new StdSchedulerFactory();
Scheduler scheduler = scheduleFactory.getScheduler();

Scheduler 是一个计划集,其中可以包含多个 JobDetail 和 Trigger 组成的计划任务。
我们可以从 SchedulerFactory 中取得 Scheduler。

接口Job是每个业务上需要执行的任务需要实现的接口,该接口只有一个方法:

public interface Job {
    public void execute(JobExecutionContext context)
        throws JobExecutionException;
}

我们可以在里面定义我们的 Job 执行逻辑,比如清除过期数据,更新缓存等。

JobDetail描述了一个任务具体的信息,比如名称,组名等等。
JobDetail jobDetail = new JobDetail("SayHelloWorldJob",Scheduler.DEFAULT_GROUP, SayHelloWorldJob.class);
在上面的构造方法中,第一个是任务的名称,第二个是组名,第三个就是实际当任务需要执行的回调类。

Trigger顾名思义就是触发器,Quartz有个很好的想法就是分离了任务和任务执行的条件。Trigger就是控制任务执行条件的类,当Trigger认为执行条件满足的时刻,Trigger会通知相关的Job去执行。分离的好处是:
1.你可以为某个Job关联多个Trigger,其中任何一个条件满足都可以触发job执行,这样可以完成一些组合的高级触发条件
2.当Trigger失效后(比如:一个永远都不能满足的条件),你不必去声明一个新的job,代替的是你可以为job关联一个新的Trigger让job可以继续执行。

目前的Quartz实现中,存在两种Trigger,SimpleTrigger和CronTrigger,SimpleTrigger用来完成一些比如固定时间执行的任务,比如:从现在开始1分钟后等等;而CronTrigger(没错,和unix的cron进程的含意一样)用来执行calendar-like的任务,比如:每周五下午3:00,每月最后一天等等。

在我们项目中,都是一些固定时间的 Job,所以只用到了 SimpleTrigger。
Trigger trigger = newSimpleTrigger("SayHelloWorldJobTrigger",Scheduler.DEFAULT_GROUP,newDate(),null,0,0L);
这个构造方法中,第一个是Trigger的名称,第二个是Trigger的组名,第三个是任务开始时间,第四个是结束时间,第五个是重复次数(使用SimpleTrigger.REPEAT_INDEFINITELY常量表示无限次),最后一个是重复周期(单位是毫秒),那么这样就创建了一个立刻并只执行一次的任务。

但我们定义好了 JobDetail,Job,和 Trigger 后,就可以开始 Schedule 一个 Job 了。

scheduler.scheduleJob(jobDetail,trigger);

这条语句就是把job和Trigger关联,这样当Trigger认为应该触发的时候就会调用(实际上是Scheduler调用)job.execute方法了。

scheduler.start();
千万别忘了加上上面的语句,这条语句通知Quartz使安排的计划生效。

关于execute方法的参数JobExecutionContext
JobExecutionContext就和很多Context结尾的类功能一样,提供的运行时刻的上下文环境,JobExecutionContext中有Scheduler,JobDetail,Trigger等很多对象的引用,从而当你在execute方法内部须需要这些对象的时刻提供的便利。

在项目中,我们把需要执行的 Job 相对应的一些信息放在 JobExecutionContext 中,在 Job 执行的时候可以调用。

jobDetail.getJobDataMap().put(userid, id);

在 Job 中,我们可以拿到相关的 Context 信息:

jobExecutionContext.getJobDetail().getJobDataMap().getInt(userid);

JobDetail和Trigger的name和group
Scheduler实例对应了很多job和trigger的实例,为了方便的区分,Quartz使用name和group这两个特性,正如你想向的一样,同一个group下不能有两个相同name的JobDetail,Trigger同理,同一个Scheduler下不能有两个相同group的JobDetail,Trigger同理,JobDetail和Trigger的完全限定名为:group + name

为了让服务器重启以后,我们的 Scheduler 信息仍然不丢失,我们通常采用数据库持久化 Scheduler 的信息。
DBScript 在 Quartz 的下载包中的:quartz-1.6.0\docs\dbTables 下,选择自己使用的 DB 相应的 Script 导入数据库就可以了。
在应用中,我们需要配置一个 quartz.properties 才能正常使用 DB。我们可以在quartz-1.6.0\examples\example10 中找到该文件的样例,稍作一些修改,就可以放到自己项目源码的根目录下使用了。

设置org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX 即可启用基于 JDBC 的 Quartz 信息持久化。

根据项目情况设置以下配置信息:
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass =org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties = false
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = false

org.quartz.dataSource.myDS.driver= com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/myapplication
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password =
org.quartz.dataSource.myDS.maxConnections = 5

但是光设置了 Database 不够,我们还需要在 Application 启动的时候自动启动 Scheduler 才行,我们只需要简单的写一个 Servlet 的 Listener 并在 web.xml 中声明该 Listener ,在 Servlet 容易启动的时候,Scheduler 就开始自动执行。

public classScheduleStartListener implements ServletContextListener {
    public void contextInitialized(ServletContextEventservletContextEvent) {
        try {
          scheduleFactory.getScheduler().start();
        } catch (SchedulerException e) {
           // write log
        }
    }

   public void contextDestroyed(ServletContextEvent servletContextEvent) {
        try {
          scheduleFactory.getScheduler().shutdown();
        } catch (SchedulerException e) {
           // write log
        }
    }
}

在 web.xml 里面加入以下配置:
<listener>
   <listener-class>org.agilejava.scheduler.ScheduleStartListener</listener-class>
</listener>

三. 配置 Quartz 使用集群 

为 Quartz 配置集群环境的步骤比设置类似的 J2EE 集群环境容易的多:

1.  配置每个节点的 quartz.properties 文件。

2.  配置 JDBC JobStore。

3.  使用 Scheduler 信息(Job 和 Trigger) 装载数据库。

4.  启动每个 Quartz 节点。

·配置节点的 quartz.properties 文件

就像是运行 Quartz 在非集群环境中那样,每个 Quartz 应用需要一个 quartz.properties 文件。在第三章,“Hello, Quartz” 中提到过,假如你不指定它,会使用默认的 quartz.properties 文件(存在于 quartz.jar文件中)。最好是指定这个文件,因为你终究是需求修改一个或多个的设置项。

当使用 Quartz 的集群特性,你需要为每个节点修改 quartz.properties 文件。代码 11.1 显示了一个用于集群实例的 quartz.properties 文件的例子。属性将在之后讨论。

代码 11.1. 集群实例的 quartz.properties 文件示例

 

1. #==============================================================  

2. #Configure Main Scheduler Properties  

3. #==============================================================   

4. org.quartz.scheduler.instanceName = TestScheduler1  

5. org.quartz.scheduler.instanceId = instance_one 

6. #==============================================================  

7. #Configure ThreadPool  

8. #==============================================================   

9. org.quartz.threadPool.class = org.quartz.simpl.Simple ThreadPool  

10.org.quartz.threadPool.threadCount = 5  

11.org.quartz.threadPool.threadPriority = 5 

12.#==============================================================  

13.#Configure JobStore  

14.#==============================================================   

15.org.quartz.jobStore.misfireThreshold = 60000  

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

17.org.quartz.jobStore.driverDelegateClass =  

18.org.quartz.impl.jdbcjobstore.MSSQLDelegate  

19.org.quartz.jobStore.tablePrefix = QRTZ_  

20.org.quartz.jobStore.dataSource = myDS  

21.  

22.org.quartz.jobStore.isClustered = true  

23.org.quartz.jobStore.clusterCheckinInterval = 20000 

24.#==============================================================  

25.#Non-Managed Configure Datasource  

26.#==============================================================   

27.org.quartz.dataSource.myDS.driver = net.sourceforge.jtds.jdbc.Driver  

28.org.quartz.dataSource.myDS.URL = jdbc:jtds:sqlserver://localhost:1433/quartz   

29.org.quartz.dataSource.myDS.user = admin  

30.org.quartz.dataSource.myDS.password = admin  

31.org.quartz.dataSource.myDS.maxConnections = 10  

C#代码 

1. #==============================================================  

2. #Configure Main Scheduler Properties  

3. #==============================================================  

4. org.quartz.scheduler.instanceName = TestScheduler1  

5. org.quartz.scheduler.instanceId = instance_one  

6. #==============================================================  

7. #Configure ThreadPool  

8. #==============================================================  

9. org.quartz.threadPool.class = org.quartz.simpl.Simple ThreadPool  

10.org.quartz.threadPool.threadCount = 5  

11.org.quartz.threadPool.threadPriority = 5  

12.#==============================================================  

13.#Configure JobStore  

14.#==============================================================  

15.org.quartz.jobStore.misfireThreshold = 60000  

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

17.org.quartz.jobStore.driverDelegateClass =  

18.org.quartz.impl.jdbcjobstore.MSSQLDelegate  

19.org.quartz.jobStore.tablePrefix = QRTZ_  

20.org.quartz.jobStore.dataSource = myDS  

21.org.quartz.jobStore.isClustered = true  

22.org.quartz.jobStore.clusterCheckinInterval = 20000  

23.#==============================================================  

24.#Non-Managed Configure Datasource  

25.#==============================================================  

26.org.quartz.dataSource.myDS.driver = net.sourceforge.jtds.jdbc.Driver  

27.org.quartz.dataSource.myDS.URL = jdbc:jtds:sqlserver://localhost:1433/quartz  

28.org.quartz.dataSource.myDS.user = admin  

29.org.quartz.dataSource.myDS.password = admin  

30.org.quartz.dataSource.myDS.maxConnections = 10  


·配置主要的 Scheduler 属性

在这一节中有两个属性应该配置

   ·org.quartz.scheduler.instanceName

   ·org.quartz.scheduler.instanceId

这两属性用于多处,用在 JDBC JobStore 中和数据库来唯一标识实例。

集群时为实例 ID 使用 AUTO

AUTO 为专门为集群准备的。不幸的是,在某些早期的版本,1.4.5 版所用的机制任何情况下都不会清理旧的实例 ID。1.5.1 版中有一些插入式的实例 ID 生成器,其中一个是基于节点的 IP 地址来生成 ID;只要你在一台给定的机器上仅有一个 Quartz 集群节点时这个生成器工作的很好。集群时应当使用 AUTO,因为很多人把 Quartz 放在 EAR 中,分布部署在集群的应用服务器上。这时候,EAR 中必然只有一个quartz.properties 文件,因此它在所有的节点上是相同的。假如实例 ID 是硬编码的,Quartz 集群将不能正常工作,因为所有的节点有着一样的 ID。AUTO 就是为解决这个问题的。

一些其他严重的集群问题在 Quartz 1.5.1 被引入。如果你需要对Quartz 集群,你或许该避免这个版本。


·配置 JobStore 块

为使用 Quartz 的集群,你需要用到 JobStoreTX 或者 JobStoreCMT 作为 Scheduler 的 JobStore。第六章,“Job 存储和持久化” 详细说明了如何设置和使用所提供的这两个 JDBC JobStore。从代码 11.1 中,你能发现和第六章所显示的设置是一样的,还有两个附加的属性:

    ·org.quartz.jobStore.isClustered

    ·org.quartz.jobStore.clusterChedkinInterval

通过设置 org.quartz.jobStore.isClustered 属性为 true,你就告诉了 Scheduler 实例要它参与到一个集群当中。这一属性会贯穿于调度框架的始终,用于修改集群环境中操作的默认行为。

org.quartz.jobStore.clusterCheckinInterval 属性定义了Scheduler 实例检入到数据库中的频率(毫秒为单位)。Scheduler 检查是否其他的实例到了它们应当检入的时候未检入;这能指出一个失败的 Scheduler 实例,且当前 Scheduler 会以此来接管任何执行失败并可恢复的 Job。通过检入操作,Scheduler 也会更新自身的状态记录。

clusterChedkinInterval 越小,Scheduler 节点检查失败的 Scheduler 实例就越频繁。默认值是 15000(即15 秒)。

 

statefuljob确实可以考虑,从实验来看,它是等待上一个周期的运行实例结束之后再启动下一个周期的实例,但问题如果实例运行持续的时间很长,比如是周期的10倍,那么quartz启动的是当前时间的实例还是补上这10个周期内没启动的实例呢? 

从实验结果来看,我把周期设为1秒,同时向源库中循环插入100000条记录,同步结束耗时78s,实例起了48个,从而推测上面的问题答案应该是启动当前时间周期的实例,而之前的忽略。但察看实例的启动时间,发现问题似乎并不完全这样,当时间间隔很小的事情下,quartz会连续把之前没启动的实例在某个周期内连续“补”回来,这一点让人费解,难道线程控制当真如此诡异?

 

 

5. 易失性、持久性和可恢复性

这三个属性有些类似的,由于它们影响的都是 Job 的运行时行为。我们下面依次讨论它们。

·Job 的易失性

一个易失性的 Job 是在程序关闭之后不会被持久化。一个 Job 是通过调用 JobDetail 的 setVolatility(true) 被设置为易失性的。

当你需要持久化 Job 时不应使用 RamJobStore

RamJobStore 使用的是非永久性存储器,所有关于 Job 和 Trigger 的信息会在程序关闭之后丢失。保存 Job 到 RamJobStore 有效的使得它们是易失性的。假如你需要让你的 Job 信息在程序重启之后仍能保留下来,你就该考虑另一种 JobStore 类型,比如 JobStoreTX 或者 JobStoreCMT。它们会在第六章“作业存储与持久化”中讲到。


Job 易失性的默认值是 false.

·Job 持久性

一个持久的 Job 是它应该保持在  JobStore 中的,甚至是在没有任何 Trigger 去触发它的时候。我们说,你设置了一个单次触发的 Trigger,触发之后它就变成了 STATE_COMPLETE 状态。Job 执行一次后就不再被触发了,这个 Trigger 部署之后只为了执行一次。这个 Trigger 指向的 Job 现在成了一个孤儿 Job,因为不再有任何 Trigger 与之相关联了。

假如你设置一个 Job 为连续性的,即使它成了孤儿 Job 也不会从 JobStore 移除掉。这样可以保证在将来,无论何时你的程序决定为这个 Job 增加另一个 Trigger 都是可用的。假如调用了 JobDetail 的setDurability(false) 方法,那么在所有的触发器触发之后 Job 将从 JobStore 中移出。连续性的默认值是 false。因此,Job 和 Trigger 的默认行为是:当 Trigger 完成了所有的触发、Job 在没有 Trigger 与之关联时它们就会从 JobStore 中移除。

·Job 的可恢复性

当一个 Job 还在执行中,Scheduler 经历了一次非预期的关闭,在 Scheduler 重启之后可恢复的 Job 还会再次被执行。这个 Job 会再次重头开始执行。Scheduler 是没法知道在程序停止的时候 Job 执行到了哪个位置,因此必须重新开始再执行一遍。

要设置 Job 为可恢复性,用下面的方法:

1.public void setRequestsRecovery(boolean shuldRecover);


默认时,这个值为 false,Scheduler 不会试着去恢复 job 的。

·从 Scheduler 中移除 Job

你可用多种方式移除已部署的 Job。一种方式是移除所有与这个 Job 相关联的 Trigger;如果这个 Job 是非持久性的,它将会从 Scheduler 中移出。一个更简单直接的方式是使用 deleteJob() 方法:

1.public boolean deleteJob(String jobName, String groupName)

2.throws SchedulerException;


·中断 Job

有时候需要能中断一个 Job,尤其是对于一个长时间执行的 Job。例如,假定你有一个 Job 运行过程要花费一个小时,你发现在 5 分钟的时候因某个非受控的错误被中断需要接着执行。你或许也会中断 Job,修复问题,然后又继续运行。

Quartz 包括一个接口叫做 org.quartz.InterruptableJob,它扩展了普通的 Job 接口并提供了一个 interrupt() 方法:

1.public void interrupt() throws

2.UnableToInterruptJobException;



可以提供 Job 部署时所用的 Job 的名称和组名调用 Scheduler 的 interrupte() 方法:

1.public boolean interrupt(SchedulingContext ctxt, String

2.jobName,String groupName) throws

3.UnableToInterruptJobException;


代码 4.8 就是一个叫做 ChedkForInterruptJob 的 InterruptableJob 例子。

Scheduler 接着会调用你的 Job 的 interrupt() 方法。这时就取决于你和你的 Job 决定如何中断 Job 了。Quartz 有几种如何处理中断的方式,代码 4.8 中提供的是比较通用的。Quartz 框架可向 Job 发出中断请求的信号,但此时是你在控制着 Job,因此需要你为中断信号作出响应。

代码 4.8. InterrupableJob 能用来决定是否调用了 Scheduler 的 interrupte()

01.public class CheckForInterruptJob implements InterruptableJob {

02.static Log logger =LogFactory.getLog(CheckForInterruptJob.class);

03. 

04.private boolean jobInterrupted = false;

05. 

06.private int counter = 5;

07. 

08.private boolean jobFinished = false;

09. 

10.public void interrupt() throws UnableToInterruptJobException {

11.jobInterrupted= true;

12.}

13. 

14.public void execute(JobExecutionContext context)

15.throws JobExecutionException {

16. 

17.while (!jobInterrupted && !jobFinished) {

18. 

19.//Perform a small amount of processing

20.logger.info("Processingthe job");

21.counter--;

22. 

23.if (counter <= 0) {

24.jobFinished= true;

25.}

26. 

27.//Sleep and wait for 3 seconds

28.try {

29.Thread.sleep(3000);

30.} catch (InterruptedException e) {

31.//do nothing

32.}

33.}

34.if (jobFinished) {

35.logger.info("Jobfinished without interrupt");

36.} else if (jobInterrupted) {

37.logger.info("Jobwas interrupted");

38.}

39.}

40.}



·框架所提供的 Job

Quartz 框架提供了几个可在你的应用程序中轻松使用的 Job,表 4.1 列出了那种 Job 及用法

表 4.1. Quartz 框架所提供的 Job

Job 类

Job 用法

org.quartz.jobs.FileScanJob

检查某个指定文件是否变化,并在文
件被改变时通知到相应监听器的 Job

org.quartz.jobs.FileScanListener

在文件被修改后通知 FileScanJob 的监听器 

org.quartz.jobs.NativeJob

用来执行本地程序(如 windows 下 .exe 文件) 的 Job

org.quartz.jobs.NoOpJob

什么也不做,但用来测试监听器不是很有用的。
一些用户甚至仅仅用它来导致一个监听器的运行

org.quartz.jobs.ee.mail.SendMailJob

使用 JavaMail API 发送 e-mail 的 Job

org.quartz.jobs.ee.jmx.JMXInvokerJob

调用 JMX bean 上的方法的 Job

org.quartz.jobs.ee.ejb.EJBInvokerJob

用来调用 EJB 上方法的 Job


6. 快速了解Java 线程

本章很短小却很有必要暂时从对 Quartz 框架的讨论中转移到这个话题来。线程在 Quartz 框架中扮演着一个很重要的角色。要是没有线程,Java(以及 Quartz) 就不得不使用重量级的进程来执行并发的任务(Job,对于 Quartz 来说)。这个材料对于已经理解了 Java 中线程如何工作的人来说是很基本的。假如你是这类人,请宽容一下。假如你还没有机会去学习有关 Java 线程的知识,现在是个相当好的时机去快速了解它。尽管主要是关注于 Java 线程的讨论,我们在后面部份会由此进一步讨论线程在 Quartz 是如何运用的。

·Java 中的线程

线程允许程序同一时间做很多任务,至少,看起来那些任务是并发执行的。本章的讨论不考虑并行处理的情形,在任一特定时刻仅有一个线程在执行,但是 CPU 给每个线程一小片时间运行(通过时间片)然后来回在线程间快速的切换。这就是我们所看到的多线程运行的样子。

Java 语言使用 Thread 类内建了对线程的支持。当一个线程被通知运行,该线程的 run() 方法就被执行。正是因为你创建了一个线程实例并且调用的是 start() 方法,所以并不意意味着相应的 run() 方法会得到立即执行;这个线程实例必须等待,直到 JVM 告诉它可以运行。

·线程的生命周期

线程在它的生命周期中会是几种可能的状态中的一种。在同一时刻只能处于一种状态,这些状态是由 JVM 指示的,而不是操作系统。线程状态列示如下:

    ·新建

    ·可运行

    ·阻塞

    ·等待中

    ·指定时间的等待

    ·结束

当一个新的线程被创建后,它就被指定为新建状态。线程在此状态时不会被分配任何系统资源。要使线程转到可运行状态,必须调用 start() 方法。

当调用了新建线程的 start() 方法,线程进入到就绪状态并被认为是运行中的。这不代表这个线程实际正在执行指令。即使线程的状态被设置为可运行,已安排去运行,它或许不得不等到其他执行中的线程结束或被临时挂起。JVM 来决定哪个线程该获得下次运行的机会。JVM 决定下个线程的行为依赖于数个因素,包括操作系统、特定的 JVM 实现和线程优先级及其他因素。图 4.4 显示了 Java 线程的生命周期。

图 4.4. Java 线程的生命周期

阻塞和等待状态对于本书来说讨论的余地不大,也是要好分几个主题才能讲清,我们在此不必对此深入。假如你想要关于这些主题更多信息的话,你可以看 Sun 站点的 Java 指南关于线程的内容,http://java.sun.com/docs/books/tutorial/essential/threads/index.html。

·进程和线程

每一个 Java 程序被指派一个进程。程序接着把任务分派到线程中。甚至是你在写一个仅包含一个 main() 方法的 "Hello,World" 程序,程序仍然要使用线程,虽然只有一个。

有时候,所谓“轻量级进程”指的就是线程。这与实际的操作系统进程“重量级”一词形成对比。假如你正在使用 Windows 系统,你可以在任务管理器中看到运行着的重量级进程,但是你看不到线程。图 4.5 描绘了一个 Java 程序中的多线程如何操作。

图 4.5. 运行在一个程序中的两个线程

从图 4.5 中看到,线程通常不会并发着运行的。这里还是先排除多 CPU 和并行处理的情况的话,同一时刻只能有一个线程才能得到 CPU 的参与执行。每一个 JVM 和操作系统对此处理方式可能有所不同,然而,有时,一个线程可能要执行完成后,另一线程才能接着运行。这就是所知的“非抢先式”。其他的,一个线程中途被打断来让其他线程做它们的事。这被称做“抢先式”。一般的,线程必须完成运行后另一线程才能获得 CPU,这种对 CPU 资源的竟争通常是基于线程优先级的。

·理解和使用线程优先级

我们已多次提到,可运行线程是否能获得下次运行机会是由 JVM 来决定的。JVM 支持的一种知名的调度方案“固定优先级调度”,它调度线程是基于它们与其他可运行线程优先级相比较而定的。

优先级是一个指定给新线程的整数值,它还应参考创建它的父线程的优先级。这个值从 Thread.MIN_PRIORITY (等于 1),到 Thread.MAX_PRIORITY (等于 10)。这些常量值在 java.lang.Thread 类中有定义。

这个值越大 (最大到 MAX_PRIORITY),所具有的优先级就越高。除非特别指定,Java 程序中多数刚创建的线程运线在 Thread.NORMAL_PRIORITY (恰好是个中值 5) 级别上。你可能用 setPriority()方法来修改线程的优先级。

通常,一个运行着的线程持续运行直到下面情况中的一个:

    ·线程让出 CPU (可能是调用了它的 sleep() 方法、yield() 方法或者是 Object.wait() 方法)。

    ·线程的 run() 结束了。

    ·OS 支持时间片的话,它的时间到了

    ·另一高优先级的线程变为了可运行状态


·守护线程

守护线程有时指的是服务线程,因为他们运行在很低的优先级别上(在后台),完成些非紧急但却本质的任务。例如,Java 的垃圾回收器就是一个守护线程的例子。这个线程运行在后台,查找并回收程序不再使用的内存。

你可以通过传递 true 给 setDaemon() 方法使线程成为守护线程。不然,这个线程就是一个用户线程。你仅能在线程启动之前把它设置为守护线程。守护线程有点特别的就是假如只有守护线程在运行而没有活着的非守护线程,此时 JVM 仍然是存在的。

· Java 线程组和线程池

Java 的 ThreadGroup 由 java.lang.ThreadGroup 类实现的,描绘的是一组行为如单一实体般的线程。每一个 Java 线程都是线程组的成员。在一个线程组中,你可以停止或启动所有的线程组成员。你还可以设置线程优先级和执行其他线程的通用功能。线程组对于构建像 Quartz 那样的多线程程序是非常有用的。

当一个 Java 程序启动后,它就创建了一个叫做 main 的 ThreadGrop。除非特别指定,所有创建的线程都会成为这个组的成员。当一个线程被指定到某个 ThreadGroup 时,它才会被改变。

·Java 线程池 (ThreadPool)

Java 1.5 引入一个新的概念到 Java 语言中,称之线程池。第一眼看来,这有些像是线程组,但是它实际是用于不同目的的。尽量多个线程能从属于一个相同的 ThreadGroup 中,组是享用不到典型的池化资源带来的好处的。这就是说,线程组仅仅是用于对成员的组织。

线程池是可共享的资源,是一个能被用来执行任务的可管理的线程集合。线程池(通常讲的资源池) 比非池化资源提供了几点好处。首先也是首要的,资源池通过减少过度的对象创建改善了性能。假如你实例化十个线程,并重复的使用它们,而不是每次需要一个都创建一个新的线程,你将会改善程序的性能。这个概念在 Java 和 J2EE 领域中比比皆是。另外的优点包括能限制资源的数量,这有助于程序的稳定性和可扩展性。这是一个非常有意义的特征,而不管你所用的是什么语言,或是什么程序。

cron表达式的格式 
QuartzCron 表达式支持到七个域

名称

是否必须

允许值

特殊字符

0-59

, - * /

0-59

, - * /

0-23

, - * /

1-31

, - * ? / L W C

1-12 或 JAN-DEC

, - * /

1-7 或 SUN-SAT

, - * ? / L C #

空 或 1970-2099

, - * /

月份和星期的名称是不区分大小写的。FRI 和 fri 是一样的。
域之间有空格分隔,这和 UNIX cron一样。无可争辩的,我们能写的最简单的表达式看起来就是这个了:
* * * ? * *
这个表达会每秒钟(每分种的、每小时的、每天的)激发一个部署的 job。
·理解特殊字符
同 UNIX cron一样,Quartzcron 表达式支持用特殊字符来创建更为复杂的执行计划。然而,Quartz 在特殊字符的支持上比标准 UNIX cron 表达式更丰富了。
* 星号
使用星号(*) 指示着你想在这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发这个 trigger。
表达式样例:
0 * 17 * * ?
意义:每天从下午5点到下午5:59中的每分钟激发一次 trigger。它停在下午 5:59 是因为值 17 在小时域上,在下午 6 点时,小时变为 18 了,也就不再理会这个 trigger,直到下一天的下午5点。
在你希望 trigger 在该域的所有有效值上被激发时使用 * 字符。
? 问号
? 号只能用在日和周域上,但是不能在这两个域上同时使用。你可以认为 ? 字符是 "我并不关心在该域上是什么值。" 这不同于星号,星号是指示着该域上的每一个值。? 是说不为该域指定值。

不能同时这两个域上指定值的理由是难以解释甚至是难以理解的。基本上,假定同时指定值的话,意义就会变得含混不清了:考虑一下,如果一个表达式在日域上有值11,同时在周域上指定了 WED。那么是要 trigger 仅在每个月的11号,且正好又是星期三那天被激发?还是在每个星期三的11号被激发呢?要去除这种不明确性的办法就是不能同时在这两个域上指定值。

只要记住,假如你为这两域的其中一个指定了值,那就必须在另一个字值上放一个 ?。

表达式样例:

0 10,44 14 ? 3 WEB

意义:在三月中的每个星期三的下午2:10 和 下午 2:44 被触发。
, 逗号
逗号 (,) 是用来在给某个域上指定一个值列表的。例如,使用值 0,15,30,45 在秒域上意味着每15秒触发一个 trigger。
表达式样例:
0 0,15,30,45 * * * ?
意义:每刻钟触发一次 trigger。


/ 斜杠

斜杠 (/) 是用于时间表的递增的。我们刚刚用了逗号来表示每15分钟的递增,但是我们也能写成这样 0/15。
表达式样例:
0/15 0/30 * * * ?
意义:在整点和半点时每15秒触发 trigger。
- 中划线
中划线 (-) 用于指定一个范围。例如,在小时域上的 3-8 意味着 "3,4,5,6,7 和 8 点。" 域的值不允许回卷,所以像 50-10 这样的值是不允许的。
表达式样例:
0 45 3-8 ? * *
意义:在上午的3点至上午的8点的45分时触发 trigger。

L 字母

L 说明了某域上允许的最后一个值。它仅被日和周域支持。当用在日域上,表示的是在月域上指定的月份的最后一天。例如,当月域上指定了 JAN 时,在日域上的 L 会促使 trigger 在1月31号被触发。假如月域上是 SEP,那么 L 会预示着在9月30号触发。换句话说,就是不管指定了哪个月,都是在相应月份的时最后一天触发 trigger。

表达式 0 0 8 L * ? 意义是在每个月最后一天的上午 8:00 触发 trigger。在月域上的 * 说明是 "每个月"。

当 L 字母用于周域上,指示着周的最后一天,就是星期六 (或者数字7)。所以如果你需要在每个月的最后一个星期六下午的 11:59 触发 trigger,你可以用这样的表达式 0 59 23 ? * L。

当使用于周域上,你可以用一个数字与 L 连起来表示月份的最后一个星期 X。例如,表达式 0 0 12 ? * 2L 说的是在每个月的最后一个星期一触发 trigger。

不要让范围和列表值与 L 连用

虽然你能用星期数(1-7)与 L 连用,但是不允许你用一个范围值和列表值与 L 连用。这会产生不可预知的结果。


W 字母

W 字符代表着平日 (Mon-Fri),并且仅能用于日域中。它用来指定离指定日的最近的一个平日。大部分的商业处理都是基于工作周的,所以 W 字符可能是非常重要的。例如,日域中的 15W 意味着 "离该月15号的最近一个平日。" 假如15号是星期六,那么 trigger 会在14号(星期四)触发,因为距15号最近的是星期一,这个例子中也会是17号(译者Unmi注:不会在17号触发的,如果是15W,可能会是在14号(15号是星期六)或者15号(15号是星期天)触发,也就是只能出现在邻近的一天,如果15号当天为平日直接就会当日执行)。W 只能用在指定的日域为单天,不能是范围或列表值。

# 井号

# 字符仅能用于周域中。它用于指定月份中的第几周的哪一天。例如,如果你指定周域的值为 6#3,它意思是某月的第三个周五 (6=星期五,#3意味着月份中的第三周)。另一个例子2#1 意思是某月的第一个星期一 (2=星期一,#1意味着月份中的第一周)。注意,假如你指定 #5,然而月份中没有第 5 周,那么该月不会触发。

遇到的问题

Quartz与slf4j(log4j)冲突,个人感觉好像很所有的日志记录都会发生冲突

 

 

 

 

 

 

如你所见,Job相当容易实现。这里只是介绍有关Jobs本质, IJob接口的Execute(..)方法以及JobDetails中需要理解的内容。

在所实现的类成为真正的“Job”时,期望任务所具有的各种属性需要通知给Quartz。通过JobDetail类可以完成这个工作,这个类在前面的章节中曾简短提及过。现在,我们花一些时间来讨论Quartz中Jobs的本质和Job实例的生命周期。首先让我们回顾一下第一课中所看到的代码片断:

// define the job and tie it to our HelloJob class
JobDetail job = new JobDetail("job1", "group1", typeof(HelloJob));
// Trigger the job to run on the next round minute
SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1", runTime);
            
// Tell quartz to schedule the job using our trigger
sched.ScheduleJob(job, trigger);

现在考虑如下定义的SimpleJob类:

public class SimpleJob : IJob
{
        
        private static ILog _log = LogManager.GetLogger(typeof(SimpleJob));
        
        /// <summary> 
        /// Empty constructor for job initilization.
        /// </summary>
        public SimpleJob()
        {
        }
        
        /// <summary>
        /// Called by the <see cref="IScheduler" /> when a
        /// <see cref="Trigger" /> fires that is associated with
        /// the <see cref="IJob" />.
        /// </summary>
        public virtual void  Execute(JobExecutionContext context)
        {
            // This job simply prints out its job name and the
            // date and time that it is running
            string jobName = context.JobDetail.FullName;
            _log.Info(string.Format("SimpleJob says: {0} executing at {1}", jobName, DateTime.Now.ToString("r")));
      }
}

注意,我们给scheduler传入了一个JobDetail实例,而且这个JobDetail实例只是简单提供了类名来引用被执行的Job。每次scheduler执行这个任务时,它就创建这个类的新实例,然后调用该实例的Execute(..)方法。对这种行为的一个推论就是Job类必须有一个无参数的构造函数。另外一个推论就是它使得Job类中定义的成员数据失去意义,因为这些成员数据值在每次执行的时候被“清空”了。

你可能要问,如何才能为每个Job实例提供属性和配置呢?而且,在执行中如何跟踪Job的状态呢?这些问题的答案是相同的:关键就是JobDataMap,这是JobDetail对象的一部分。

JobDataMap

JobDataMap被用来保存一系列的(序列化的)对象,这些对象在Job执行时可以得到。JobDataMap是IDictionary接口的一个实现,而且还增加了一些存储和读取主类型数据的便捷方法。

下面是将Job加入到scheduler前使用的一些向JobDataMap加入数据的方法。

// pass initialization parameters into the job
job1.JobDataMap.Put(ColorJob.FAVORITE_COLOR, "Green");
job1.JobDataMap.Put(ColorJob.EXECUTION_COUNT, 1);

下面的代码展示了在Job执行过程中从JobDataMap 获取数据的代码:

/// <summary>
    /// This is just a simple job that receives parameters and
    /// maintains state
    /// </summary>
    /// <author>Bill Kratzer</author>
    public class ColorJob : IStatefulJob
    {
        
        private static ILog _log = LogManager.GetLogger(typeof(ColorJob));
        
        // parameter names specific to this job
        public const string FAVORITE_COLOR = "favorite color";
        public const string EXECUTION_COUNT = "count";
        
        // Since Quartz will re-instantiate a class every time it
        // gets executed, members non-static member variables can
        // not be used to maintain state!
        private int _counter = 1;
        
    
        /// <summary>
        /// Called by the <see cref="IScheduler" /> when a
        /// <see cref="Trigger" /> fires that is associated with
        /// the <see cref="IJob" />.
        /// </summary>
        public virtual void Execute(JobExecutionContext context)
        {
            
            // This job simply prints out its job name and the
            // date and time that it is running
            string jobName = context.JobDetail.FullName;
            
            // Grab and print passed parameters
            JobDataMap data = context.JobDetail.JobDataMap;
            string favoriteColor = data.GetString(FAVORITE_COLOR);
            int count = data.GetInt(EXECUTION_COUNT);
            _log.Info(string.Format("ColorJob: {0} executing at {1}\n  favorite color is {2}\n  execution count (from job map) is {3}\n  execution count (from job member variable) is {4}", 
                jobName, DateTime.Now.ToString("r"), favoriteColor, count, _counter));
            
            // increment the count and store it back into the 
            // job map so that job state can be properly maintained
            count++;
            data.Put(EXECUTION_COUNT, count);
            
            // Increment the local member variable 
            // This serves no real purpose since job state can not 
            // be maintained via member variables!
            _counter++;
        }

    } 

如果使用一个持久的JobStore(在本指南的JobStore章节中讨论),那么必须注意存放在JobDataMap中的内容。因为放入JobDataMap中的内容将被序列化,而且容易出现类型转换问题。很明显,标准.NET类型将是非常安全的,但除此之外的类型,任何时候,只要有人改变了你要序列化其实例的类的定义,就要注意是否打破了程序的兼容性。另外,你可以对JobStore和JobDataMap采用一种使用模式:就是只把主类型和String类型存放在Map中,这样就可以减少后面序列化的问题。

有状态和无状态任务

Triggers也可以有JobDataMaps与之相关联。当scheduler中的Job被多个有规律或者重复触发的Triggers所使用时非常有用。对于每次独立的触发,你可为Job提供不同的输入数据。

从Job执行时的JobExecutionContext中取得JobDataMap是惯用手段,它融合了从JobDetail和从Trigger中获的JobDataMap,当有相同名字的键时,它用后者的值覆盖前者值。

StatefulJob——有状态任务

现在,一些关于Job状态数据的附加论题:一个Job实例可以被定义为“有状态的”或者“无状态的”。“无状态的”任务只拥有它们被加入到scheduler时所存储的JobDataMap。这意味着,在执行任务过程中任何对Job Data Map所作的更改都将丢失而且任务下次执行时也无法看到。你可能会猜想出,有状态的任务恰好相反,它在任务的每次执行之后重新存储JobDataMap。有状态任务的一个副作用就是它不能并发执行。换句话说,如果任务有状态,那么当触发器在这个任务已经在执行的时候试图触发它,这个触发器就会被阻塞(等待),直到前面的执行完成。

想使任务有状态,它就要实现IStatefulJob接口而不是实现IJob接口。

Job 'Instances' 任务“实例”

这个课程的最终观点或许已经很明确,可以创建一个单独的Job类,并且通过创建多个JobDetails实例来将它的多个实例存储在scheduler中,这样每个JobDetails对象都有它自己的一套属性和JobDataMap,而且将它们都加入到scheduler中。

当触发器被触发的时候,通过Scheduler中配置的JobFactory来实例化与之关联的Job类。缺省的JobFactory只是简单地对Job类调用GetScheduler()方法。创建自己JobFactory可以利用应用中诸如Ioc或者DI容器所产生或者初始化的Job实例。

obs的其它属性

这里简短地总结一下通过JobDetail对象可以定义Job的其它属性。

  • Durability(持久性)-如果一个Job是不持久的, 一旦没有触发器与之关联,它就会被从scheduler 中自动删除。

  • Volatility(无常性)-如果一个Job是无常的,在重新启动Quartz i scheduler 时它不能被保持。 

  • RequestsRecovery(请求恢复能力) -如果一个Job具备“请求恢复”能力,当它在执行时遇到scheduler “硬性的关闭”(例如:执行的过程崩溃,或者计算机被关机),那么当scheduler重新启动时,这个任务会被重新执行。这种情况下,JobExecutionContext.Recovering 属性将是true。

  • JobListeners(任务监听器) -一个Job如果有0个或者多个JobListeners监听器与之相关联,当这个Job执行时,监听器被会被通知。更多有关JobListeners的讨论见TriggerListeners & JobListeners章节。

 

JobExecutionException 任务执行异常

最后,需要告诉你一些关于Job.Execute(..)方法的细节。在Execute方法被执行时,仅允许抛出一个JobExecutionException类型异常。因此需要将整个要执行的内容包括在一个'try-catch'块中。应花费一些时间仔细阅读JobExecutionException文档,因为Job能够使用它向scheduler提供各种指示,你也可以知道怎么处理异常。

 

 

 

 

Quartz的开发使用 

1.      作为应用程序开发

程序代码如下

package com.ada.apps;

import java.text.ParseException;

import org.quartz.CronTrigger;

import org.quartz.JobDetail;

import org.quartz.Scheduler;

import org.quartz.SchedulerException;

import org.quartz.SchedulerFactory;

import org.quartz.impl.StdSchedulerFactory;

import com.quartz.WorkJob;

public class QuartzApps {

 

    /**

     * @param args

     * @throwsSchedulerException

     * @throws ParseException

     * @throwsInterruptedException

     */

    public static void main(String[] args) throws SchedulerException,

           ParseException, InterruptedException {

       SchedulerFactory factory = new StdSchedulerFactory();

       Scheduler sched = factory.getScheduler();// 初始计划处理集程序

       // 新建一个任务

       JobDetail job = new JobDetail("mywok", Scheduler.DEFAULT_GROUP,

              WorkJob.class);

       // 新建一个任务触发器

       //0/3 * * * * ? * cron表达式 当前的意思是每三秒钟执行一次

       CronTrigger cronTrigger = new CronTrigger("trigger3",

              Scheduler.DEFAULT_GROUP, "0/3 * * * *? *");

       // 想计划处理集中添加任务和任务触发条件

       sched.scheduleJob(job, cronTrigger);

       // 启动计划处理集程序

       sched.start();

       String[] ggStrings = sched.getJobNames(Scheduler.DEFAULT_GROUP);

       //罗列出当前所有的job的名字

       for (String item : ggStrings) {

           System.out.println(item);

       }

       Thread.sleep(10000);

       //十秒钟后关闭quartz计划处理集程序

       sched.shutdown();

    }

 

}

 

 

与spring整合

 

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

<!DOCTYPE beans PUBLIC "-//SPRING//DTDBEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>

    <!--起动Bean-->

    <bean id="z"

       class="org.springframework.scheduling.quartz.SchedulerFactoryBean">

       <property name="triggers">

           <list>

              <ref bean="cronReportTrigger"/>

           </list>

       </property>

    </bean>

 

    <!--实际的工作Bean-->

    <bean id="courseService" class="com.spring.helloworld.CourseService"></bean>

    <!--jobBean用于设定启动时运用的Bean与方法-->

    <bean id="scheduledReportJobDetail"

       class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">

       <property name="targetObject">

           <ref bean="courseService"/>

       </property>

       <property name="targetMethod">

           <value>sendCourseEnrollmentReport</value>

       </property>

    </bean>

    <!--定时器设定起动频率&启动时间我设的是每5秒起动一次 (0 0  4 * * ?每日四点起动....)-->

    <bean id="cronReportTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">

        <property name="jobDetail">

           <ref bean="scheduledReportJobDetail"/>

       </property>

       <property name="cronExpression">

           <value>10,15,20,25,30,35,40,45,50,55 * * * * ?</value>

       </property>

    </bean>

 

</beans> 

3 在web容器中运行

一般情况下在web.xml添加一个quartz的监视器

配置如下

config-file代表quartz的属性文件

<context-param>

       <param-name>config-file</param-name>

       <param-value>quartz.properties</param-value>

    </context-param>

<listener>

    <listener-class>org.quartz.ee.servlet.QuartzInitializerListener</listener-class>

</listener>

 

属性文件配置如下

org.quartz.scheduler.instanceName = PushDBScheduler    

org.quartz.scheduler.instanceId= one  

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

org.quartz.threadPool.threadCount= 4    

org.quartz.threadPool.threadPriority= 4   

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

org.quartz.plugin.jobInitializer.fileNames = quartz_job.xml

 

 

 

quartz_job.xml配置文件如下

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

<quartz>

    <job>

        <job-detail>

            <name>ScanItemsInDB</name>

            <group>Scanning</group>

            <job-class>com.quartz.ConsoleJob</job-class>实现了Job接口的类

        </job-detail>

        <trigger>

            <cron>

                <name>t1</name>

                <group>Scanning</group>

                <job-name>ScanItemsInDB</job-name>

                <job-group>Scanning</job-group>

                <cron-expression>0/5 * * * * ?</cron-expression>

            </cron>

        </trigger>

    </job>

</quartz>

 

 

4 给quartz添加一些全局的监视器

 

 

 

开发中出现的异常

Based on configured schedule, the given triggerwill never fire.

 

原因:被quartz调度的某个任务job的触发者cronTrigger的触发时间通过cron表达式配置了个不合法或过去的时间;该job永远不会被执行到

 

 

 

项目中的需要的表达式

0 30 23 ? * SUN *    每周的星期天晚上23点 30分 0秒的时候执行一次

0 30 23 l * ? *        每月的最后一天晚上3点 30分 0秒的时候执行一次

0 30 23 l 12 ? *       每年的最后一天晚上3点 30分 0秒的时候执行一次

关于项目周乐捐的时间添加 执行的时间 到执行时间-1000*60*60*24*7的时间段

关于项目月乐捐的时间添加 执行的时间 到执行时间-1000*60*60*24*month的时间段month代表当前月的天数

望设计添加计划时间是用毫秒数,用毫秒数不存在跨年跨月问题