quartz与timer

来源:互联网 发布:幕墙下料软件 编辑:程序博客网 时间:2024/06/18 10:02


1、timer与quartz的比较

1)精确度和功能

   Quartz可以通过cron表达式精确到特定时间执行,而TimerTask不能。Quartz拥有TimerTask所有的功能,而TimerTask则没有。

 

2)任务类的数量

    TimerTask和Quartz每次执行任务时,每次调用的是不是都是同一个任务类对象,还是每次都不一样?现在做如下实验,每次执行任务时,将任务类对象本身打印出来。

   Quartz每次执行任务都创建一个新的任务类对象,而TimerTask则每次使用同一个任务类对象。

3)对异常的处理

一个循环执行的任务,如果某一次执行任务时,因为某些原因抛出异常,则定时器是否还会在下一个执行任务的时间点执行任务吗?下面通过模拟在任务类中抛出异常,来模拟这种情况,并测试两种定时器如何处理这种情况。

Quartz的某次执行任务过程中抛出异常,不影响下一次任务的执行,当下一次执行时间到来时,定时器会再次执行任务;而TimerTask则不同,一旦某个任务在执行过程中抛出异常,则整个定时器生命周期就结束,以后永远不会再执行定时器任务



总结:其实timer实现定时任务是很简单的,但是在想法开发是很少用到timer,而是用spring的Quartz。我也在网上找到了一些资料,现在总结一下。

1. Java定时器没有持久化机制。
2. Java定时器的日程管理不够灵活(只能设置开始时间、重复的间隔,设置特定的日期、时间等)//这点感同身受
3. Java定时器没有使用线程池(每个Java定时器使用一个线程)//想必在用timer是遇到了吧。

4. Java定时器没有切实的管理方案,你不得不自己完成存储、组织、恢复任务的措施


2、各种定时器的实现(timer、quartz的程序直接启动与web监听两种方式)

1定时器的作用
在实际的开发中,如果项目中需要定时执行或者需要重复执行一定的工作,定时器显现的尤为重要。
当然如果我们不了解定时器就会用线程去实现,例如:
package org.lzstone.action
public class FinanceAction extends Thread{
       private Date date;
       public void run{
       try{
       while(true){
       Thread.sleep((int)(Math.random()*1000));
       date = new Date();
       //定时执行任务
       }
       }catch(Exception e){
        e.printStackTrace();
       }
}
}
自己实现定时器的工作很复杂,如果实现不好占用内存过多,系统就此Over,所以处理定时执行或者重复执行的任务,定时器是很好的选择
2.java中常见的定时器
1)借助Java.util.Timer来实现
2)OpenSymphony社区提供的Quartz来实现
3.介绍Timer
利用Timer开发定时任务是主要分为两个步骤:
1)创建定时任务类
示例代码:
package org.lzstone.action
import java.util.TimeTask
public class LzstoneTimeTask extends TimeTask{
       public void run(){
              //执行的定时器任务
       }
}
2)运行定时任务,运行定时任务分为两种方式:
2.1)程序直接启动
示例代码:
package org.lzstone.action
public class LzstoneMain{
       .......
       public void run(){
        //执行定时器的任务
        //创建实例
        Timer timer = new Timer();
        参数:
        new LzstoneTimeTask()- 所要安排的任务。
        0- 执行任务前的延迟时间,单位是毫秒。
        1*1000- 执行各后续任务之间的时间间隔,单位是毫秒。
        timer.schedule(new LzstoneTimeTask(),0,1*1000);
       }
}
2.2)web监听方式
示例代码:
package org.lzstone.action
public class LzstoneMain implements ServletContextListener{
       private Timer timer = null;
       //初始化监听器,创建实例,执行任务
       public void contextInitialized(ServletContextEvent event){
               timer = new Timer();
               timer.schedule(new LzstoneTimeTask(),0,1*1000);
       }
       //销毁监听器,停止执行任务
       public void contextDestroyed(ServletContextEvent event){
              //注意,在此计时器调用的计时器任务的 run 方法内调用此方法,就可以绝对确保正在执行的任务是此计时器所执行的最后一个任务。
              timer.cancel();
        }
}
web.xml配置
<listener>
   <listener-class>
        org.lzstone.action.LzstoneMain
   </listener-class>
</listener>
4. 介绍Quartz
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,可以用来创建简单或者复杂的定时任务,利用Quartz开发定时任务的步骤与Timer类

似。

利用Quartz开发定时任务是主要分为两个步骤:
1)创建定时任务类
示例代码:
package org.lzstone.action
public class LzstoneTimeTask implements Job{
       public void execute(JobExecutionContext context) throws JobExecutionException{
              //执行的定时器任务
       }
}
2)运行定时任务,运行定时任务分为两种方式:
2.1)程序直接启动,创建任务调度器及配置相应的任务计划
示例代码:
package org.lzstone.action
public class LzstoneMain{
       private static Scheduler sched;
       public static void run() throws Exception{
              //创建LzstoneTimeTask的定时任务
              JobDetail jobDetail = new JobDetail("lzstoneJob",sched.DEFAULT_GROUP,LzstoneTimeTask.class);
              //目标 创建任务计划
              CronTrigger trigger = new CronTrigger("lzstoneTrigger","lzstone","0 0 12 * * ?");
              //0 0 12 * * ? 代表每天的中午12点触发
              sched = new org.quartz.impl.StdSchedulerFactory().getScheduler();
              sched.scheduleJob(jobDetail,trigger);
              sched.start();
       }
       //停止
       public static void stop() throws Exception{
              sched.shutdown();
        }
}
//执行
public class Main{
       .............
       public void run(){
            LzstoneMain.run();
       }
       ............
}
2.2)web监听方式
示例代码:
package org.lzstone.action
public class LzstoneMainListener implements ServletContextListener{
       private Timer timer = null;
       //初始化监听器,创建实例,执行任务
       public void contextInitialized(ServletContextEvent event){
               LzstoneMain.run();
       }
       //销毁监听器,停止执行任务
       public void contextDestroyed(ServletContextEvent event){
              LzstoneMain.stop();
        }
}
web.xml配置
<listener>
   <listener-class>
        org.lzstone.action.LzstoneMainListener
   </listener-class>
</listener>
5.对比
Timer方式实现定时器,原理简单,实现方便,在执行简单的任务比较方便,不足之处是无法确定执行时间,并且依赖性比较强,必须继承指定的类
Quartz方式实现定时器,方便,清晰指定启动时间,定时参数比较灵活,容易实现比较复杂的定时任务,不足之处是需要实现特定接口,加载其框架
两种方式各有优缺点,在特定场合可以根据其特点选择使用。
6.Spring定时任务
Spring定时任务对Timer与Quartz都提供了支持,并且实现步骤基本一样
首先配置Spring对Timer的支持
1.1 创建定时任务类
package org.lzstone.action
import java.util.TimeTask
public class LzstoneTimeTask extends TimeTask{
       public void run(){
              //执行的定时器任务
       }
}
1.2 注册定时任务类,配置任务计划与任务调度器
    在项目的WEB-INF下面创建TimerConfig.xml文件(一般叫做applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean>
<!--注册定时执行任务实体-->
<bean id="lzstoneTimeTask" class="org.lzstone.action.LzstoneTimeTask"/>
<!--注册定时器信息-->
<bean id="taskInfo" class="org.springframework.scheduling.timer.ScheduledTimerTask">
<!--第一次执行任务前需要等待的时间,这里设置为3秒-->
<property name="delay">
<value>3000</value>
</property>
<!--设置任务的执行周期 这里设置为4秒-->
<property name="period">
  <value>4000</value>
</property>
<!--设置具体执行的任务 这里设置为lzstoneTimeTask-->
<property name="timerTask">
<ref local="lzstoneTimeTask"/>
</property>
</bean>
<!--配置定时器任务的调度器-->
<bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean">
<!--注册定时器列表-->
<property name="scheduledTimerTasks">
    <list>
        <ref local="taskInfo"/>
        ........
    </list>
</property>
</bean>
</beans>
1.3 web项目中的启动设置
    <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/TimerConfig.xml</param-value>
     </context-param>

     <listener>
         <listener-class>
                  org.springframework.web.context.ContextLoaderListener
         </listener-class>
     </listener>
配置Spring对Quartz的支持
2.1 创建定时任务类
package org.lzstone.action
public class LzstoneQuartzTask{
       public void execute(){
              //执行的定时器任务
       }
}
2.2 注册定时任务类,配置任务计划与任务调度器
    在项目的WEB-INF下面创建QuartzConfig.xml文件(一般叫做applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean>
<!--注册定时执行任务实体-->
<bean id="lzstoneQuartzTask" class="org.lzstone.action.LzstoneQuartzTask"/>
<!--注册定时器信息-->
<bean id="taskInfo" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!--指定要执行的定时任务类  这里是LzstoneQuartzTask-->
<property name="targetObject">
<ref local="lzstoneQuartzTask"/>
</property>
<!--指定定时器任务类要执行的方法名称 这里是execute-->
<property name="targetMethod">
<value>execute</value>
</property>
</bean>
<!--配置定时器任务的调度器-->
<bean id="quartzTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<!--声明要运行的实体-->
<property name="jobDetail">
    <ref local="taskInfo"/>
</property>
<!--设置运行时间-->
<property name="cronExpression">
    <value>0 0 12 * * ?</value>
</property>
</bean>
<!--注册监听器-->
<bean id="registerQuartz" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<!--注册定时器实体 集合-->
<property name="triggers">
    <list>
          <ref local="quartzTrigger"/>
    </list>
</property>
</bean>
</beans>
2.3 web项目中的启动设置
    <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/QuartzConfig.xml</param-value>
     </context-param>

     <listener>
         <listener-class>
                  org.springframework.web.context.ContextLoaderListener
         </listener-class>
     </listener>


4、quartz原理

概述

各种企业应用几乎都会碰到任务调度的需求,就拿论坛来说:每隔半个小时生成精华文章的RSS文件,每天凌晨统计论坛用户的积分排名,每隔30分钟执行锁定用户解锁任务。

对于一个典型的MIS系统来说,在每月1号凌晨统计上个月各部门的业务数据生成月报表,每半个小时查询用户是否已经有快到期的待处理业务……,这样的例子俯拾皆是,不胜枚举。

任务调度本身涉及到多线程并发、运行时间规则制定和解析、场景保持与恢复、线程池维护等诸多方面的工作。如果直接使用自定义线程这种刀耕火种的原始办法,开发任务调度程序是一项颇具挑战性的工作。Java开源的好处就是:领域问题都能找到现成的解决方案。

OpenSymphony所提供的Quartz2001年发布版本以来已经被众多项目作为任务调度的解决方案,Quartz在提供巨大灵活性的同时并未牺牲其简单性,它所提供的强大功能使你可以应付绝大多数的调度需求。

Quartz 在开源任务调度框架中的翘首,它提供了强大任务调度机制,难能可贵的是它同时保持了使用的简单性。Quartz 允许开发人员灵活地定义触发器的调度时间表,并可以对触发器和任务进行关联映射。

此外,Quartz提供了调度运行环境的持久化机制,可以保存并恢复调度现场,即使系统因故障关闭,任务调度现场数据并不会丢失。此外,Quartz还提供了组件式的侦听器、各种插件、线程池等功能。

了解Quartz体系结构

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Scheduler结构图

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

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

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

使用SimpleTrigger

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

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

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

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

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


Cron表达式

Quartz使用类似于Linux下的Cron表达式定义时间规则,Cron表达式由67个由空格分隔的时间字段组成,如表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”,则表示从1012点,即10,11,12

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

斜杠(/)x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,3045秒,而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表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。

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年的每天1015运行

"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 ? 3 WED"

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

"0 15 10 ? * MON-FRI"

每周一,二,三,四,五的10:15分运行。

"0 15 10 15 * ?"

每月1510:15分运行。

"0 15 10 L * ?"

每月最后一天10:15分运行。

"0 15 10 ? * 6L"

每月最后一个星期五10:15分运行。

"0 15 10 ? * 6L 2007-2009"

2007,2008,2009年每个月的最后一个星期五的10:15分运行。

"0 15 10 ? * 6#3"

每月第三个星期五的10:15分运行。


代码示例

[java] view plain copy
  1. package com.lxq.quartz.test;  
  2.   
  3. import java.util.Date;  
  4.   
  5. import org.quartz.Job;  
  6. import org.quartz.JobExecutionContext;  
  7. import org.quartz.JobExecutionException;  
  8.   
  9. public class BaseJob implements Job  
  10. {  
  11.   
  12.     @Override  
  13.     public void execute(JobExecutionContext JEContext) throws JobExecutionException  
  14.     {  
  15.         /*输出当前job执行的时间*/  
  16.         System.out.println(new Date(System.currentTimeMillis()) + "我在执行");  
  17.         /*输出当前jobDetail的name,groupName和JobClass*/  
  18.         System.out.println(JEContext.getJobDetail().getName() + "," + JEContext.getJobDetail().getGroup() + ","  
  19.                 + JEContext.getJobDetail().getJobClass());  
  20.         /*输出当前jobDetail的JobDataMap中的数据*/  
  21.         System.out.println(JEContext.getJobDetail().getJobDataMap().get("type"));  
  22.         System.out.println(JEContext.getJobDetail().getJobDataMap().get("index"));  
  23.         /*输出当前trigger的JobDataMap中的数据*/  
  24.         System.out.println(JEContext.getTrigger().getJobDataMap().get("type"));  
  25.         System.out.println(JEContext.getTrigger().getJobDataMap().get("index"));  
  26.     }  
  27. }  

[java] view plain copy
  1. package com.lxq.quartz.test;  
  2.   
  3. import java.util.Date;  
  4. import java.util.GregorianCalendar;  
  5.   
  6. import org.quartz.CronTrigger;  
  7. import org.quartz.JobDetail;  
  8. import org.quartz.Scheduler;  
  9. import org.quartz.SchedulerException;  
  10. import org.quartz.SchedulerFactory;  
  11. import org.quartz.SimpleTrigger;  
  12. import org.quartz.TriggerUtils;  
  13. import org.quartz.impl.StdSchedulerFactory;  
  14. import org.quartz.impl.calendar.AnnualCalendar;  
  15.   
  16. public class TestQuartz  
  17. {  
  18.     public static void main(String[] args) throws SchedulerException  
  19.     {  
  20.         TestQuartz testQuartz=new TestQuartz();  
  21.         testQuartz.test3();  
  22.           
  23.     }  
  24.   
  25.     public void test1()  
  26.     {  
  27.         //通过SchedulerFactory来获取一个调度器  
  28.         SchedulerFactory schedulerFactory = new StdSchedulerFactory();  
  29.         Scheduler scheduler;  
  30.         try  
  31.         {  
  32.             scheduler = schedulerFactory.getScheduler();  
  33.             //引进作业程序public JobDetail(String jobName, String jobGroupName, Class jobClass){}  
  34.             JobDetail jobDetail = new JobDetail("jobDetail-j1""jobGroup-g1", BaseJob.class);  
  35.             /*new一个触发器public SimpleTrigger(String triggerName, String triggerGroupName){} 
  36.             在构造Trigger实例时,可以考虑使用org.quartz.TriggerUtils工具类,该工具类不但提供了众多获取特定时间的方法,还拥有众多获取常见Trigger的方法, 
  37.             如makeSecondlyTrigger(String trigName)方法将创建一个每秒执行一次的Trigger,而makeWeeklyTrigger(String trigName, int dayOfWeek, int hour, int minute) 
  38.             将创建一个每星期某一特定时间点执行一次的Trigger。而getEvenMinuteDate(Date date)方法将返回某一时间点一分钟以后的时间。*/  
  39.             SimpleTrigger simpleTrigger = new SimpleTrigger("simpleTrigger""triggerGroup-tg1");  
  40.             //设置作业启动时间,马上启动  
  41.             long ctime = System.currentTimeMillis();  
  42.             simpleTrigger.setStartTime(new Date(ctime));  
  43.             //设置作业执行间隔   
  44.             simpleTrigger.setRepeatInterval(1000);  
  45.             //设置作业执行次数  
  46.             simpleTrigger.setRepeatCount(10);  
  47.             //设置作业执行优先级默认为5  
  48.             simpleTrigger.setPriority(10);  
  49.             //作业和触发器注册到调度器中,并建立Trigger和JobDetail的关联  
  50.             scheduler.scheduleJob(jobDetail, simpleTrigger);  
  51.             /*上面的一句话,也可以换成下面的这几句话 
  52.              * JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class); 
  53.             SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1"); 
  54.             simpleTrigger.setJobGroup("jGroup1");:指定关联的Job组名 
  55.             simpleTrigger.setJobName("job1_1");指定关联的Job名称 
  56.             scheduler.addJob(jobDetail, true);注册JobDetail 
  57.             scheduler.scheduleJob(simpleTrigger);注册指定了关联JobDetail的Trigger*/  
  58.             //启动调度器  
  59.             scheduler.start();  
  60.         }  
  61.         catch (SchedulerException e)  
  62.         {  
  63.             e.printStackTrace();  
  64.         }  
  65.     }  
  66.   
  67.     public void test2()  
  68.     {  
  69.         try  
  70.         {  
  71.             //通过SchedulerFactory来获取一个调度器  
  72.             SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();  
  73.               
  74.             Scheduler sched = schedFact.getScheduler();  
  75.             JobDetail jobDetail = new JobDetail("jobDetail-j2""jobGroup-g1", BaseJob.class);  
  76.             jobDetail.getJobDataMap().put("type""JobDetail");  
  77.             jobDetail.getJobDataMap().put("index"1002);  
  78.               
  79.             /*public CronTrigger(){} 
  80.             public CronTrigger(String name, String group){} 
  81.             public CronTrigger(String name, String group, String cronExpression){}*/  
  82.             CronTrigger trigger = new CronTrigger("cronTrigger""triggerGroup-tg2");  
  83.             /*CronTrigger 能够提供比 SimpleTrigger 更有具体实际意义的调度方案,调度规则基于 Cron 表达式,CronTrigger  
  84.             支持日历相关的重复时间间隔(比如每月第一个周一执行),而不是简单的周期时间间隔。*/  
  85.             trigger.getJobDataMap().put("type""trigger");  
  86.             trigger.getJobDataMap().put("index"2002);  
  87.             /* 每半分钟执行一次  */  
  88.             trigger.setCronExpression("30 * * * * ?");  
  89.             sched.scheduleJob(jobDetail, trigger);  
  90.             sched.start();  
  91.         }  
  92.         catch (Exception e)  
  93.         {  
  94.             e.printStackTrace();  
  95.         }  
  96.     }  
  97.   
  98.     /*在实际任务调度中,我们不可能一成不变地按照某个周期性的调度规则运行任务,必须考虑到实现生活中日历上特定日期,就象习惯了大男人作风的人在2月14号也会有不同表现一样。*/  
  99.     public void test3() throws SchedulerException  
  100.     {  
  101.         SchedulerFactory sf = new StdSchedulerFactory();  
  102.         Scheduler scheduler = sf.getScheduler();  
  103.         /*法定节日是以每年为周期的,所以使用AnnualCalendar*/  
  104.         AnnualCalendar holidays = new AnnualCalendar();  
  105.         /*五一劳动节*/  
  106.         GregorianCalendar laborDay = new GregorianCalendar();  
  107.         laborDay.add(GregorianCalendar.MONTH,5);  
  108.         laborDay.add(GregorianCalendar.DATE,1);  
  109.         holidays.setDayExcluded(laborDay, true); //排除的日期,如果设置为false则为包含  
  110.         /*国庆节*/  
  111.         GregorianCalendar nationalDay = new GregorianCalendar();  
  112.         nationalDay.add(GregorianCalendar.MONTH,10);  
  113.         nationalDay.add(GregorianCalendar.DATE,1);  
  114.         holidays.setDayExcluded(nationalDay, true);//排除该日期  
  115.         /*Scheduler#addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers) 
  116.          进行注册,如果updateTriggers为true,Scheduler中已引用Calendar的Trigger将得到更新*/  
  117.         scheduler.addCalendar("holidays", holidays, falsefalse);//向Scheduler注册日历  
  118.         Date runDate = TriggerUtils.getDateOf(0,01017);//7月1号 上午10点  
  119.         JobDetail job = new JobDetail("job1""group1", BaseJob.class);  
  120.         SimpleTrigger trigger = new SimpleTrigger("trigger1""group1",runDate,null,SimpleTrigger.REPEAT_INDEFINITELY,60L * 60L * 1000L);  
  121.         trigger.setCalendarName("holidays");//让Trigger应用指定的日历规则  
  122.         scheduler.scheduleJob(job, trigger);  
  123.         scheduler.start();  
  124.   
  125.     }  
  126.   
  127. }  

任务调度信息存储

在默认情况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。比如我们希望安排一个执行100次的任务,如果执行到50次时系统崩溃了,系统重启时任务的执行计数器将从0开始。在大多数实际的应用中,我们往往并不需要保存任务调度的现场数据,因为很少需要规划一个指定执行次数的任务。

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

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

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

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

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

quartz.properties:默认配置

[html] view plain copy
  1. ①集群的配置,这里不使用集群  
  2. org.quartz.scheduler.instanceName = DefaultQuartzScheduler  
  3. org.quartz.scheduler.rmi.export = false  
  4. org.quartz.scheduler.rmi.proxy = false  
  5. org.quartz.scheduler.wrapJobExecutionInUserTransaction = false  
  6. ②配置调度器的线程池  
  7. org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool  
  8. org.quartz.threadPool.threadCount = 10  
  9. org.quartz.threadPool.threadPriority = 5  
  10. org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true  
  11. ③配置任务调度现场数据保存机制  
  12. org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore  

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

1)集群信息;

2)调度器线程池;

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

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

quartz.properties:使用数据库保存任务调度现场数据

[html] view plain copy
  1. org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX  
  2. org.quartz.jobStore.tablePrefix = QRTZ_①数据表前缀  
  3. org.quartz.jobStore.dataSource = qzDS②数据源名称  
  4. ③定义数据源的具体属性  
  5. org.quartz.dataSource.qzDS.driver = oracle.jdbc.driver.OracleDriver  
  6. org.quartz.dataSource.qzDS.URL = jdbc:oracle:thin:@localhost:1521:ora9i  
  7. org.quartz.dataSource.qzDS.user = stamen  
  8. org.quartz.dataSource.qzDS.password = abc  
  9. org.quartz.dataSource.qzDS.maxConnections = 10  

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

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

查询数据库中的运行信息

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

[java] view plain copy
  1. package com.lxq.quartz.test;  
  2.   
  3. import java.util.Date;  
  4. import org.quartz.JobDetail;  
  5. import org.quartz.Scheduler;  
  6. import org.quartz.SchedulerFactory;  
  7. import org.quartz.SimpleTrigger;  
  8. import org.quartz.impl.StdSchedulerFactory;  
  9.   
  10. public class SimpleTriggerRunner  
  11. {  
  12.     public static void main(String args[])  
  13.     {  
  14.         try  
  15.         {  
  16.             JobDetail jobDetail = new JobDetail("job1_1""jGroup1", BaseJob.class);  
  17.             SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1""tgroup1");  
  18.             simpleTrigger.setStartTime(new Date());  
  19.             simpleTrigger.setRepeatInterval(2000);  
  20.             simpleTrigger.setRepeatCount(100);  
  21.             SchedulerFactory schedulerFactory = new StdSchedulerFactory();  
  22.             Scheduler scheduler = schedulerFactory.getScheduler();  
  23.             scheduler.scheduleJob(jobDetail, simpleTrigger);  
  24.             scheduler.start();  
  25.         }  
  26.         catch (Exception e)  
  27.         {  
  28.             e.printStackTrace();  
  29.         }  
  30.   
  31.     }  
  32.   
  33. }  
[java] view plain copy
  1. package com.lxq.quartz.test;  
  2.   
  3. import java.util.Date;  
  4.   
  5. import org.quartz.Scheduler;  
  6. import org.quartz.SchedulerFactory;  
  7. import org.quartz.SimpleTrigger;  
  8. import org.quartz.Trigger;  
  9. import org.quartz.impl.StdSchedulerFactory;  
  10.   
  11. public class JDBCJobStoreRunner  
  12. {  
  13.     public static void main(String args[])  
  14.     {  
  15.         try  
  16.         {  
  17.             SchedulerFactory schedulerFactory = new StdSchedulerFactory();  
  18.             Scheduler scheduler = schedulerFactory.getScheduler();  
  19.             //获取调度器中所有的触发器组  
  20.             String[] triggerGroups = scheduler.getTriggerGroupNames();  
  21.             //重新恢复在tgroup1组中,名为trigger1_1触发器的运行  
  22.             for (int i = 0; i < triggerGroups.length; i++)  
  23.             {  
  24.                 String[] triggers = scheduler.getTriggerNames(triggerGroups[i]);  
  25.                 for (int j = 0; j < triggers.length; j++)  
  26.                 {  
  27.                     Trigger tg = scheduler.getTrigger(triggers[j], triggerGroups[i]);  
  28.                     if (tg instanceof SimpleTrigger && tg.getFullName().equals("tgroup1.trigger1_1"))//根据名称判断  
  29.                     {  
  30.                         /*恢复运行public Date rescheduleJob(String triggerName, String groupName, Trigger newTrigger){}*/  
  31.                         scheduler.rescheduleJob(triggers[j], triggerGroups[i], tg);  
  32.                     }  
  33.                 }  
  34.             }  
  35.             scheduler.start();  
  36.         }  
  37.         catch (Exception e)  
  38.         {  
  39.             e.printStackTrace();  
  40.         }  
  41.     }  
  42. }  


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

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

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


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

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


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

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



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


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

值得注意的是,如果你使用JDBC保存任务调度数据时,当你运行代码SimpleTriggerRunner然后退出,当再次希望运行SimpleTriggerRunner时,系统将抛出JobDetail重名的异常:Unable to store Job with name: 'job1_1' and group: 'jGroup1', because one already exists with this identification.

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

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

附录

·org.quartz.scheduler.instanceName 
每个 Scheduler 必须给定一个名称来标识。当在同一个程序中有多个实例时,这个名称作为客户代码识别是哪个 Scheduler 而用。假如你用到了集群特性,你就必须为集群中的每一个实例使用相同的名称,以使它们成为“逻辑上” 是同一个 Scheduler 。
·org.quartz.scheduler.instanceId
每个 Quartz Scheduler 必须指定一个唯一的 ID。这个值可以是任何字符串值,只要对于所有的 Scheduler 是唯一的。如果你想要自动生成的 ID,那你可以使用AUTO 作为 instanceId 。从版本 1.5.1 开始,你能够定制如何自动生成实例 ID。见instanceIDGenerator.class 属性,会在接下来讲到。
·org.quartz.scheduler.instanceIdGenerator.class
从版本 1.5.1 开始,这个属性允许你定制instanceId 的生成,这个属性仅被用于属性org.quartz.scheduler.instanceId 设置为AUTO 的情况下。默认是 org.quartz.simpl.SimpleInstanceIdGenerator ,它会基于主机名和时间戳来产生实例 ID 的。
·org.quartz.scheduler.threadName
可以是对于 Java 线程来说有效名称的任何字符串。假如这个属性未予指定,线程将会接受 Scheduler 名称 (org.quartz.scheduler.instanceName ) 前附加上字符串 '_QuartzSchedulerThread' 作为名称。
·org.quartz.scheduler.idelWaitTime 
这个属性设置了当 Scheduler 处于空闲时转而再次查询可用 Trigger 时所等待的毫秒数。通常,你无需调整这个参数,除非你正使用 XA 事物,遇到了 Trigger 本该立即触发而发生延迟的问题。
·org.quartz.scheduler.dbFailureRetryInterval
这个属性设置 Scheduler 在检测到 JobStore 到某处的连接(比如到数据库的连接) 断开后,再次尝试连接所等待的毫秒数。这个参数在使用 RamJobStore 无效。
·org.quartz.scheduler.classLoadHelper.class
对于多数健状的应用,所使用的默认值为 org.quartz.simpl.CascadingClassLoadHelper 类,它会依序使用其他的ClassLoadHelper 类,直到有一个能正常工作为止。你大概没必须为这个属性指定任何其他的类,除非有可能在应用服务器中时。当前所有可能的ClassLoadHelper 实现可在 org.quartz.simpl 包中找到。
·org.quartz.context.key.SOME_KEY 
这个属性用于向 "Scheduler 上下文" 中置入一个 名-值 对表示的字符串值。(见 Scheduler.getContext() )。因此,比如设置了org.quartz.context.key.MyEmail = myemail@somehost.com就相当于执行了scheduler.getContext().put("MyEmail", myemail@somehost.com) 
·org.quartz.scheduler.userTransactionURL
它设置了 Quartz 能在哪里定位到应用服务器的 UserTransaction 管理器的 JNDI URL。默认值(未设定的话) 是java:comp/UserTransaction ,这几乎能工作于所有的应用服务器中。Websphere 用户也许需要设置这个属性为jta/usertransaction 。这个属性仅用于 Quartz 配置使用 JobStoreCMT 的情况,并且org.quartz.scheduler.wrapJobExecutionInUserTransaction 被设定成了true 。
·org.quartz.scheduler.wrapJobExecutionInUserTransaction
如果你要 Quartz 在调用你的 Job 的 execute 之前启动一个 UserTransaction 的话,设置这个属性为 true 。这个事物将在 Job 的execute 方法完成和 JobDataMap (假如是一个StatefulJob ) 更新后提交。默认值为 false 。
·org.quartz.scheduler.jobFactory.class
这是所用的 JobFactory 的类名称。默认为 org.quartz.simpl.SimpleJobFactory 。你也可以试试org.quartz.simpl.PropertySettingJobFactory 。一个 Job 工厂负责产生 Job 类的实例。SimpleFactory 类是调用 Job 类的newInstance() 方法。PropertySettingJobFactory 也会调用newInstance() ,但还会使用 JobDataMap 中的内容以反射方式设置 Job Bean 的属性。

小结

Quartz提供了最为丰富的任务调度功能,不但可以制定周期性运行的任务调度方案,还可以让你按照日历相关的方式进行任务调度。Quartz框架的重要组件包括JobJobDetailTriggerScheduler以及辅助性的JobDataMapSchedulerContextQuartz拥有一个线程池,通过线程池为任务提供执行线程,你可以通过配置文件对线程池进行参数定制。Quartz的另一个重要功能是可将任务调度信息持久化到数据库中,以便系统重启时能够恢复已经安排的任务。此外,Quartz还拥有完善的事件体系,允许你注册各种事件的监听器。

5、quartz原理2

Quartz可以用来做什么?

Quartz是一个任务调度框架。比如你遇到这样的问题

  • 想每月25号,信用卡自动还款
  • 想每年4月1日自己给当年暗恋女神发一封匿名贺卡
  • 想每隔1小时,备份一下自己的爱情动作片 学习笔记到云盘

这些问题总结起来就是:在某一个有规律的时间点干某件事。并且时间的触发的条件可以非常复杂(比如每月最后一个工作日的17:50),复杂到需要一个专门的框架来干这个事。 Quartz就是来干这样的事,你给它一个触发条件的定义,它负责到了时间点,触发相应的Job起来干活。

一个简单的示例

这里面的所有例子都是基于Quartz 2.2.1

package com.test.quartz;import static org.quartz.DateBuilder.newDate;import static org.quartz.JobBuilder.newJob;import static org.quartz.SimpleScheduleBuilder.simpleSchedule;import static org.quartz.TriggerBuilder.newTrigger;import java.util.GregorianCalendar;import org.quartz.JobDetail;import org.quartz.Scheduler;import org.quartz.Trigger;import org.quartz.impl.StdSchedulerFactory;import org.quartz.impl.calendar.AnnualCalendar;public class QuartzTest {    public static void main(String[] args) {        try {            //创建scheduler            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();            //定义一个Trigger            Trigger trigger = newTrigger().withIdentity("trigger1", "group1") //定义name/group                .startNow()//一旦加入scheduler,立即生效                .withSchedule(simpleSchedule() //使用SimpleTrigger                    .withIntervalInSeconds(1) //每隔一秒执行一次                    .repeatForever()) //一直执行,奔腾到老不停歇                .build();            //定义一个JobDetail            JobDetail job = newJob(HelloQuartz.class) //定义Job类为HelloQuartz类,这是真正的执行逻辑所在                .withIdentity("job1", "group1") //定义name/group                .usingJobData("name", "quartz") //定义属性                .build();            //加入这个调度            scheduler.scheduleJob(job, trigger);            //启动之            scheduler.start();            //运行一段时间后关闭            Thread.sleep(10000);            scheduler.shutdown(true);        } catch (Exception e) {            e.printStackTrace();        }    }}
package com.test.quartz;import java.util.Date;import org.quartz.DisallowConcurrentExecution;import org.quartz.Job;import org.quartz.JobDetail;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;public class HelloQuartz implements Job {    public void execute(JobExecutionContext context) throws JobExecutionException {        JobDetail detail = context.getJobDetail();        String name = detail.getJobDataMap().getString("name");        System.out.println("say hello to " + name + " at " + new Date());    }}

这个例子很好的覆盖了Quartz最重要的3个基本要素:

  • Scheduler:调度器。所有的调度都是由它控制。
  • Trigger: 定义触发的条件。例子中,它的类型是SimpleTrigger,每隔1秒中执行一次(什么是SimpleTrigger下面会有详述)。
  • JobDetail & Job: JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中,例子中是HelloQuartz。 为什么设计成JobDetail + Job,不直接使用Job?这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail & Job 方式,sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。

Quartz API

Quartz的API的风格在2.x以后,采用的是DSL风格(通常意味着fluent interface风格),就是示例中newTrigger()那一段东西。它是通过Builder实现的,就是以下几个。(** 下面大部分代码都要引用这些Builder ** )

//job相关的builderimport static org.quartz.JobBuilder.*;//trigger相关的builderimport static org.quartz.TriggerBuilder.*;import static org.quartz.SimpleScheduleBuilder.*;import static org.quartz.CronScheduleBuilder.*;import static org.quartz.DailyTimeIntervalScheduleBuilder.*;import static org.quartz.CalendarIntervalScheduleBuilder.*;//日期相关的builderimport static org.quartz.DateBuilder.*;

DSL风格写起来会更加连贯,畅快,而且由于不是使用setter的风格,语义上会更容易理解一些。对比一下:

JobDetail jobDetail=new JobDetailImpl("jobDetail1","group1",HelloQuartz.class);jobDetail.getJobDataMap().put("name", "quartz");SimpleTriggerImpl trigger=new SimpleTriggerImpl("trigger1","group1");trigger.setStartTime(new Date());trigger.setRepeatInterval(1);trigger.setRepeatCount(-1);

关于name和group

JobDetail和Trigger都有name和group。

name是它们在这个sheduler里面的唯一标识。如果我们要更新一个JobDetail定义,只需要设置一个name相同的JobDetail实例即可。

group是一个组织单元,sheduler会提供一些对整组操作的API,比如 scheduler.resumeJobs()。

Trigger

在开始详解每一种Trigger之前,需要先了解一下Trigger的一些共性。

StartTime & EndTime

startTime和endTime指定的Trigger会被触发的时间区间。在这个区间之外,Trigger是不会被触发的。

** 所有Trigger都会包含这两个属性 **

优先级(Priority)

当scheduler比较繁忙的时候,可能在同一个时刻,有多个Trigger被触发了,但资源不足(比如线程池不足)。那么这个时候比剪刀石头布更好的方式,就是设置优先级。优先级高的先执行。

需要注意的是,优先级只有在同一时刻执行的Trigger之间才会起作用,如果一个Trigger是9:00,另一个Trigger是9:30。那么无论后一个优先级多高,前一个都是先执行。

优先级的值默认是5,当为负数时使用默认值。最大值似乎没有指定,但建议遵循Java的标准,使用1-10,不然鬼才知道看到【优先级为10】是时,上头还有没有更大的值。

Misfire(错失触发)策略

类似的Scheduler资源不足的时候,或者机器崩溃重启等,有可能某一些Trigger在应该触发的时间点没有被触发,也就是Miss Fire了。这个时候Trigger需要一个策略来处理这种情况。每种Trigger可选的策略各不相同。

这里有两个点需要重点注意:

  • MisFire的触发是有一个阀值,这个阀值是配置在JobStore的。比RAMJobStore是org.quartz.jobStore.misfireThreshold。只有超过这个阀值,才会算MisFire。小于这个阀值,Quartz是会全部重新触发。

所有MisFire的策略实际上都是解答两个问题:

  1. 已经MisFire的任务还要重新触发吗?
  2. 如果发生MisFire,要调整现有的调度时间吗?

比如SimpleTrigger的MisFire策略有:

  • MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY

    这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。

    比如,SimpleTrigger每15秒执行一次,而中间有5分钟时间它都MisFire了,一共错失了20个,5分钟后,假设资源充足了,并且任务允许并发,它会被一次性触发。

    这个属性是所有Trigger都适用。

  • MISFIRE_INSTRUCTION_FIRE_NOW

    忽略已经MisFire的任务,并且立即执行调度。这通常只适用于只执行一次的任务。

  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

    将startTime设置当前时间,立即重新调度任务,包括的MisFire的

  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT

    类似MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT,区别在于会忽略已经MisFire的任务

  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT

    在下一次调度时间点,重新开始调度任务,包括的MisFire的

  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT

    类似于MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT,区别在于会忽略已经MisFire的任务。

  • MISFIRE_INSTRUCTION_SMART_POLICY

    所有的Trigger的MisFire默认值都是这个,大致意思是“把处理逻辑交给聪明的Quartz去决定”。基本策略是,

    1. 如果是只执行一次的调度,使用MISFIRE_INSTRUCTION_FIRE_NOW
    2. 如果是无限次的调度(repeatCount是无限的),使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
    3. 否则,使用MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

MisFire的东西挺繁杂的,可以参考这篇

Calendar

这里的Calendar不是jdk的java.util.Calendar,不是为了计算日期的。它的作用是在于补充Trigger的时间。可以排除或加入某一些特定的时间点。

以”每月25日零点自动还卡债“为例,我们想排除掉每年的2月25号零点这个时间点(因为有2.14,所以2月一定会破产)。这个时间,就可以用Calendar来实现。

例子:

AnnualCalendar cal = new AnnualCalendar(); //定义一个每年执行Calendar,精度为天,即不能定义到2.25号下午2:00java.util.Calendar excludeDay = new GregorianCalendar();excludeDay.setTime(newDate().inMonthOnDay(2, 25).build());cal.setDayExcluded(excludeDay, true);  //设置排除2.25这个日期scheduler.addCalendar("FebCal", cal, false, false); //scheduler加入这个Calendar//定义一个TriggerTrigger trigger = newTrigger().withIdentity("trigger1", "group1")     .startNow()//一旦加入scheduler,立即生效    .modifiedByCalendar("FebCal") //使用Calendar !!    .withSchedule(simpleSchedule()        .withIntervalInSeconds(1)         .repeatForever())     .build();

Quartz体贴地为我们提供以下几种Calendar,注意,所有的Calendar既可以是排除,也可以是包含,取决于:

  • HolidayCalendar。指定特定的日期,比如20140613。精度到天。
  • DailyCalendar。指定每天的时间段(rangeStartingTime, rangeEndingTime),格式是HH:MM[:SS[:mmm]]。也就是最大精度可以到毫秒。
  • WeeklyCalendar。指定每星期的星期几,可选值比如为java.util.Calendar.SUNDAY。精度是天。
  • MonthlyCalendar。指定每月的几号。可选值为1-31。精度是天
  • AnnualCalendar。 指定每年的哪一天。使用方式如上例。精度是天。
  • CronCalendar。指定Cron表达式。精度取决于Cron表达式,也就是最大精度可以到秒。

Trigger实现类

Quartz有以下几种Trigger实现:

SimpleTrigger

指定从某一个时间开始,以一定的时间间隔(单位是毫秒)执行的任务。

它适合的任务类似于:9:00 开始,每隔1小时,执行一次。

它的属性有:

  • repeatInterval 重复间隔
  • repeatCount 重复次数。实际执行次数是 repeatCount+1。因为在startTime的时候一定会执行一次。** 下面有关repeatCount 属性的都是同理。 **

例子:

simpleSchedule()        .withIntervalInHours(1) //每小时执行一次        .repeatForever() //次数不限        .build();simpleSchedule()    .withIntervalInMinutes(1) //每分钟执行一次    .withRepeatCount(10) //次数为10次    .build();

CalendarIntervalTrigger

类似于SimpleTrigger,指定从某一个时间开始,以一定的时间间隔执行的任务。 但是不同的是SimpleTrigger指定的时间间隔为毫秒,没办法指定每隔一个月执行一次(每月的时间间隔不是固定值),而CalendarIntervalTrigger支持的间隔单位有秒,分钟,小时,天,月,年,星期

相较于SimpleTrigger有两个优势:1、更方便,比如每隔1小时执行,你不用自己去计算1小时等于多少毫秒。 2、支持不是固定长度的间隔,比如间隔为月和年。但劣势是精度只能到秒。

它适合的任务类似于:9:00 开始执行,并且以后每周 9:00 执行一次

它的属性有:

  • interval 执行间隔
  • intervalUnit 执行间隔的单位(秒,分钟,小时,天,月,年,星期)

例子:

calendarIntervalSchedule()    .withIntervalInDays(1) //每天执行一次    .build();calendarIntervalSchedule()    .withIntervalInWeeks(1) //每周执行一次    .build();

DailyTimeIntervalTrigger

指定每天的某个时间段内,以一定的时间间隔执行任务。并且它可以支持指定星期。

它适合的任务类似于:指定每天9:00 至 18:00 ,每隔70秒执行一次,并且只要周一至周五执行。

它的属性有:

  • startTimeOfDay 每天开始时间
  • endTimeOfDay 每天结束时间
  • daysOfWeek 需要执行的星期
  • interval 执行间隔
  • intervalUnit 执行间隔的单位(秒,分钟,小时,天,月,年,星期)
  • repeatCount 重复次数

例子:

dailyTimeIntervalSchedule()    .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) //第天9:00开始    .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(16, 0)) //16:00 结束     .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) //周一至周五执行    .withIntervalInHours(1) //每间隔1小时执行一次    .withRepeatCount(100) //最多重复100次(实际执行100+1次)    .build();dailyTimeIntervalSchedule()    .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) //第天9:00开始    .endingDailyAfterCount(10) //每天执行10次,这个方法实际上根据 startTimeOfDay+interval*count 算出 endTimeOfDay    .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) //周一至周五执行    .withIntervalInHours(1) //每间隔1小时执行一次    .build();

CronTrigger

适合于更复杂的任务,它支持类型于Linux Cron的语法(并且更强大)。基本上它覆盖了以上三个Trigger的绝大部分能力(但不是全部)—— 当然,也更难理解。

它适合的任务类似于:每天0:00,9:00,18:00各执行一次。

它的属性只有:

  • Cron表达式。但这个表示式本身就够复杂了。下面会有说明。

例子:

cronSchedule("0 0/2 8-17 * * ?") // 每天8:00-17:00,每隔2分钟执行一次    .build();cronSchedule("0 30 9 ? * MON") // 每周一,9:30执行一次.build();weeklyOnDayAndHourAndMinute(MONDAY,9, 30) //等同于 0 30 9 ? * MON     .build();

Cron表达式

位置时间域允许值特殊值1秒0-59, - * /2分钟0-59, - * /3小时0-23, - * /4日期1-31, - * ? / L W C5月份1-12, - * /6星期1-7, - * ? / L C #7年份(可选)1-31, - * /

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

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

减号(-):表达一个范围,如在小时字段中使用“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表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。

一些例子:

表示式说明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 ? 3 WED3月每周三的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 ? * 6L 2007-2009在2007,2008,2009年每个月的最后一个星期五的10:15分运行。0 15 10 ? * 6#3每月第三个星期五的10:15分运行。

JobDetail & Job

JobDetail是任务的定义,而Job是任务的执行逻辑。在JobDetail里会引用一个Job Class定义。一个最简单的例子

public class JobTest {    public static void main(String[] args) throws SchedulerException, IOException {           JobDetail job=newJob()               .ofType(DoNothingJob.class) //引用Job Class               .withIdentity("job1", "group1") //设置name/group               .withDescription("this is a test job") //设置描述               .usingJobData("age", 18) //加入属性到ageJobDataMap               .build();           job.getJobDataMap().put("name", "quertz"); //加入属性name到JobDataMap           //定义一个每秒执行一次的SimpleTrigger           Trigger trigger=newTrigger()                   .startNow()                   .withIdentity("trigger1")                   .withSchedule(simpleSchedule()                       .withIntervalInSeconds(1)                       .repeatForever())                   .build();           Scheduler sche=StdSchedulerFactory.getDefaultScheduler();           sche.scheduleJob(job, trigger);           sche.start();           System.in.read();           sche.shutdown();    }}public class DoNothingJob implements Job {    public void execute(JobExecutionContext context) throws JobExecutionException {        System.out.println("do nothing");    }}

从上例我们可以看出,要定义一个任务,需要干几件事:

  1. 创建一个org.quartz.Job的实现类,并实现实现自己的业务逻辑。比如上面的DoNothingJob。
  2. 定义一个JobDetail,引用这个实现类
  3. 加入scheduleJob

Quartz调度一次任务,会干如下的事:

  1. JobClass jobClass=JobDetail.getJobClass()
  2. Job jobInstance=jobClass.newInstance()。所以Job实现类,必须有一个public的无参构建方法。
  3. jobInstance.execute(JobExecutionContext context)。JobExecutionContext是Job运行的上下文,可以获得Trigger、Scheduler、JobDetail的信息。

也就是说,每次调度都会创建一个新的Job实例,这样的好处是有些任务并发执行的时候,不存在对临界资源的访问问题——当然,如果需要共享JobDataMap的时候,还是存在临界资源的并发访问的问题。

JobDataMap

Job都次都是newInstance的实例,那我怎么传值给它? 比如我现在有两个发送邮件的任务,一个是发给"liLei",一个发给"hanmeimei",不能说我要写两个Job实现类LiLeiSendEmailJob和HanMeiMeiSendEmailJob。实现的办法是通过JobDataMap。

每一个JobDetail都会有一个JobDataMap。JobDataMap本质就是一个Map的扩展类,只是提供了一些更便捷的方法,比如getString()之类的。

我们可以在定义JobDetail,加入属性值,方式有二:

newJob().usingJobData("age", 18) //加入属性到ageJobDataMap or job.getJobDataMap().put("name", "quertz"); //加入属性name到JobDataMap

然后在Job中可以获取这个JobDataMap的值,方式同样有二:

public class HelloQuartz implements Job {    private String name;    public void execute(JobExecutionContext context) throws JobExecutionException {        JobDetail detail = context.getJobDetail();        JobDataMap map = detail.getJobDataMap(); //方法一:获得JobDataMap        System.out.println("say hello to " + name + "[" + map.getInt("age") + "]" + " at "                           + new Date());    }    //方法二:属性的setter方法,会将JobDataMap的属性自动注入    public void setName(String name) {         this.name = name;    }}

对于同一个JobDetail实例,执行的多个Job实例,是共享同样的JobDataMap,也就是说,如果你在任务里修改了里面的值,会对其他Job实例(并发的或者后续的)造成影响。

除了JobDetail,Trigger同样有一个JobDataMap,共享范围是所有使用这个Trigger的Job实例。

Job并发

Job是有可能并发执行的,比如一个任务要执行10秒中,而调度算法是每秒中触发1次,那么就有可能多个任务被并发执行。

有时候我们并不想任务并发执行,比如这个任务要去”获得数据库中所有未发送邮件的名单“,如果是并发执行,就需要一个数据库锁去避免一个数据被多次处理。这个时候一个@DisallowConcurrentExecution解决这个问题。

就是这样

public class DoNothingJob implements Job {    @DisallowConcurrentExecution    public void execute(JobExecutionContext context) throws JobExecutionException {        System.out.println("do nothing");    }}

注意,@DisallowConcurrentExecution是对JobDetail实例生效,也就是如果你定义两个JobDetail,引用同一个Job类,是可以并发执行的。

JobExecutionException

Job.execute()方法是不允许抛出除JobExecutionException之外的所有异常的(包括RuntimeException),所以编码的时候,最好是try-catch住所有的Throwable,小心处理。

其他属性

  • Durability(耐久性?)

    如果一个任务不是durable,那么当没有Trigger关联它的时候,它就会被自动删除。

  • RequestsRecovery

    如果一个任务是"requests recovery",那么当任务运行过程非正常退出时(比如进程崩溃,机器断电,但不包括抛出异常这种情况),Quartz再次启动时,会重新运行一次这个任务实例。

    可以通过JobExecutionContext.isRecovering()查询任务是否是被恢复的。

Scheduler

Scheduler就是Quartz的大脑,所有任务都是由它来设施。

Schduelr包含一个两个重要组件: JobStore和ThreadPool。

JobStore是会来存储运行时信息的,包括Trigger,Schduler,JobDetail,业务锁等。它有多种实现RAMJob(内存实现),JobStoreTX(JDBC,事务由Quartz管理),JobStoreCMT(JDBC,使用容器事务),ClusteredJobStore(集群实现)、TerracottaJobStore(什么是Terractta)。

ThreadPool就是线程池,Quartz有自己的线程池实现。所有任务的都会由线程池执行。

SchedulerFactory

SchdulerFactory,顾名思义就是来用创建Schduler了,有两个实现:DirectSchedulerFactory和 StdSchdulerFactory。前者可以用来在代码里定制你自己的Schduler参数。后者是直接读取classpath下的quartz.properties(不存在就都使用默认值)配置来实例化Schduler。通常来讲,我们使用StdSchdulerFactory也就足够了。

SchdulerFactory本身是支持创建RMI stub的,可以用来管理远程的Scheduler,功能与本地一样,可以远程提交个Job什么的。

DirectSchedulerFactory的创建接口

    /**     * Same as     * {@link DirectSchedulerFactory#createScheduler(ThreadPool threadPool, JobStore jobStore)},     * with the addition of specifying the scheduler name and instance ID. This     * scheduler can only be retrieved via     * {@link DirectSchedulerFactory#getScheduler(String)}     *     * @param schedulerName     *          The name for the scheduler.     * @param schedulerInstanceId     *          The instance ID for the scheduler.     * @param threadPool     *          The thread pool for executing jobs     * @param jobStore     *          The type of job store     * @throws SchedulerException     *           if initialization failed     */    public void createScheduler(String schedulerName,            String schedulerInstanceId, ThreadPool threadPool, JobStore jobStore)        throws SchedulerException;

StdSchdulerFactory的配置例子, 更多配置,参考Quartz配置指南:

org.quartz.scheduler.instanceName = DefaultQuartzSchedulerorg.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPoolorg.quartz.threadPool.threadCount = 10 org.quartz.threadPool.threadPriority = 5org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = trueorg.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

这里未讲的稍微高级的主题

  • JobStore 介绍、配置
  • 集群: 介绍、配置
  • RMI
  • 监听器 TriggerListeners and JobListeners、SchedulerListeners
  • 插件