1.什么是Quartz
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,作用是用来执行任务调度。然而任务调度本身涉及多线程并发、运行舒缓规则制定及解析、运行现场保持与恢复、以及线程池维护等很多工作。如果我们自己去开发,难度很大,而quartz工具为我们提供了便利。可以让我们创建多个Job任务。
2.开发环境搭建
引入maven的jar包:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>1.8.6</version>
</dependency>
3.quartz的基础结构
quartz提供了强大任务调度机制。允许我们灵活的定义触发器的调度时间表。然后也可以灵活得将触发器和任务进行关联映射。最主要的就是调度器、任务和触发器三大块。
Job接口:定义的方法execute,通过该方法完成需要执行的任务。JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息都保存在JobDataMap实例中。子接口StatefulJob代表有状态的任务。目得是让Quartz知道任务的类型。状态好比一把锁,无状态的Job拥有自己的JobDataMap的复制,对JobDataMap的更改不会影响下次的执行。然而有状态的Job会共享同一个JobDataMap实例,所以因此总结出无状态的Job是可以并发的,而有状态的Job是不可以并发的。
JobDetail类:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不是直接接收一个Job实例,而是接收了一个Job实现类,方便运行时通过newInstance的反射调用机制实例化Job。所以通过这个JobDetail类就可以描述Job的实现类及相关的静态信息。它的构造器需要指定Job的实现类以及任务在Scheduler调度者中的组名和Job名称。
Trigger类:描述触发Job执行的时间触发规则。子类SimpleTrigger:当仅需要触发一次或者以固定间隔周期性执行。
子类CronTrigger:通过Cron表达式定义出复杂的调度方案。
Calendar类:它是一些日历特定的时间点集合。并且一个Trigger触发器可以和多个Calendar相关联。
Scheduler类:代表一个Quartz独立运行容器,可以让Trigger和JobDetail注册到Scheduler中。它们分别在Scheduler中对应了各自的组和名称。并且这些组和名称必须唯一。Scheduler将Trigger触发器绑定到JobDetail上,这样Trigger就会触发Job的执行。相互的关系是一个Job可以被多个Trigger触发器触发,但是一个触发器只能触发一个Job。至于Scheduler实例是通过SchedulerFactory工厂以工厂模式创建的。
ThreadPool:Scheduler类使用一个线程池作为任务运行的基础设施。任务通过共享线程池中的线程来提高运行效率。
4.SimpleTrigger触发器解析
实例:
任务类:
public class MyTask implements Job{
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// TODO Auto-generated method stub
System.out.println(context.getTrigger().getName()+" trigger time :"+new Date());
}
}
调度执行类:
public class SimpleTriggerTest {
public static void main(String[] args) {
try {
JobDetail jobDetail =new JobDetail("job1","jgroup1",MyTask.class);
SimpleTrigger simpleTrigger =new SimpleTrigger("trigger1","tgroup1");
simpleTrigger.setStartTime(new Date());
//每两秒执行一次
simpleTrigger.setRepeatInterval(2000);
//重复执行10次,那么就是一共执行11次了
simpleTrigger.setRepeatCount(10);
SchedulerFactory factory =new StdSchedulerFactory();
Scheduler scheduler =factory.getScheduler();
scheduler.scheduleJob(jobDetail, simpleTrigger);
scheduler.start();
} catch (SchedulerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
结果:
5.CronTrigger触发器解析
Cron表达式中的特殊字符:
(1) * :可用在所有字段中,表示对应时间域的每一个时刻。
(2) ? :该字符只在日期和星期字段中使用。通常都当作占位符。
(3) - :表示一个范围,如时间1-3代表值为1,2,3.
(4) , :表示一个列表值。
(5) / :x/y表示一个等步长的序列,x为起始值,y为增量步长值。
(6) L :该字符只在日期和星期字段中使用。该字段用在月份中代表月份中的最后一天。该字段用在星期中代表星期六。6L代表该月的最后一个星期五,因为星期六代表最后一天,星期日代表一个月中的第一天。
(7) W:该字符只能出现在日期字段里,是对前导日期的修饰。表示离该日期最近的工作日。
(8)LW:在日期字段中可以组合使用LW,意思是当月的最后一个工作日。
(9) # :该字符只能在星期字段中使用,表示当月的某个工作日。
(10) C :该字符只在日期和星期字段中使用。
实例:
任务类:
public class MyTask implements Job{
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// TODO Auto-generated method stub
System.out.println(context.getTrigger().getName()+" trigger time :"+new Date());
}
}
任务调度类:
public class CronTriggerTest {
public static void main(String[] args) {
try {
JobDetail jobDetail =new JobDetail("job2","jgroup2",MyTask.class);
CronTrigger cronTrigger =new CronTrigger("trigger2","tgroup2");
CronExpression exp =new CronExpression("0/2 * * * * ?");
cronTrigger.setCronExpression(exp);
SchedulerFactory factroy =new StdSchedulerFactory();
Scheduler scheduler =factroy.getScheduler();
scheduler.scheduleJob(jobDetail, cronTrigger);
scheduler.start();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
结果:
说明:由于调度器寄生于主线程,而主线程如果退出的话,那么调度器中的任务就会被销毁。
6.任务调度信息的存储之默认内存数据库
默认情况下,Quartz将任务调度的运行信息保存在内存中。都知道保存在内存中数据的访问速度加快,但是却缺乏了持久性。为了保持数据的持久性,就要使用数据库进行存储相关的数据。
将quartz.properties文件放到类路径下,这样就不会加载默认的quartz.properties文件,而是加载我们定义的quartz.properties文件。
内存数据库:
quartz.propeties文件
# ===========================================================================
# Configure Main Scheduler Properties 调度器属性
# ===========================================================================
#集群的配置,这里不使用集群
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
# ===========================================================================
# Configure ThreadPool 线程池属性
# ===========================================================================
#线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)
org.quartz.threadPool.threadCount=10
#设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority=5
#设置SimpleThreadPool的一些属性
#设置是否为守护线程
#org.quartz.threadpool.makethreadsdaemons = false
#org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
#org.quartz.threadpool.threadsinheritgroupofinitializingthread=false
#线程前缀默认值是:[Scheduler Name]_Worker
#org.quartz.threadpool.threadnameprefix=swhJobThead;
# 配置全局监听(TriggerListener,JobListener) 则应用程序可以接收和执行 预定的事件通知
# ===========================================================================
# Configuring a Global TriggerListener 配置全局的Trigger监听器
# MyTriggerListenerClass 类必须有一个无参数的构造函数,和 属性的set方法,目前2.2.x只支持原始数据类型的值(包括字符串)
# ===========================================================================
#org.quartz.triggerListener.NAME.class = com.swh.MyTriggerListenerClass
#org.quartz.triggerListener.NAME.propName = propValue
#org.quartz.triggerListener.NAME.prop2Name = prop2Value
# ===========================================================================
# Configure JobStore 存储调度信息(工作,触发器和日历等)
# ===========================================================================
# 信息保存时间 默认值60秒
org.quartz.jobStore.misfireThreshold=60000
#保存job和Trigger的状态信息到内存中的类
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore
# ===========================================================================
# Configure SchedulerPlugins 插件属性 配置
# ===========================================================================
# 自定义插件
#org.quartz.plugin.NAME.class = com.swh.MyPluginClass
#org.quartz.plugin.NAME.propName = propValue
#org.quartz.plugin.NAME.prop2Name = prop2Value
#配置trigger执行历史日志(可以看到类的文档和参数列表)
org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingTriggerHistoryPlugin
org.quartz.plugin.triggHistory.triggerFiredMessage = Trigger {1}.{0} fired job {6}.{5} at: {4, date, HH:mm:ss MM/dd/yyyy}
org.quartz.plugin.triggHistory.triggerCompleteMessage = Trigger {1}.{0} completed firing job {6}.{5} at {4, date, HH:mm:ss MM/dd/yyyy} with resulting trigger instruction code: {9}
#配置job调度插件 quartz_jobs(jobs and triggers内容)的XML文档
#加载 Job 和 Trigger 信息的类 (1.8之前用:org.quartz.plugins.xml.JobInitializationPlugin)
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin
#指定存放调度器(Job 和 Trigger)信息的xml文件,默认是classpath下quartz_jobs.xml
#org.quartz.plugin.jobInitializer.fileNames = my_quartz_job2.xml
#org.quartz.plugin.jobInitializer.overWriteExistingJobs = false
#org.quartz.plugin.jobInitializer.failOnFileNotFound = true
#自动扫描任务单并发现改动的时间间隔,单位为秒
#org.quartz.plugin.jobInitializer.scanInterval = 10
#覆盖任务调度器中同名的jobDetail,避免只修改了CronExpression所造成的不能重新生效情况
#org.quartz.plugin.jobInitializer.wrapInUserTransaction = false
# ===========================================================================
# Sample configuration of ShutdownHookPlugin ShutdownHookPlugin插件的配置样例
# ===========================================================================
#org.quartz.plugin.shutdownhook.class = \org.quartz.plugins.management.ShutdownHookPlugin
#org.quartz.plugin.shutdownhook.cleanShutdown = true
#
# Configure RMI Settings 远程服务调用配置
#
#如果你想quartz-scheduler出口本身通过RMI作为服务器,然后设置“出口”标志true(默认值为false)。
#org.quartz.scheduler.rmi.export = false
#主机上rmi注册表(默认值localhost)
#org.quartz.scheduler.rmi.registryhost = localhost
#注册监听端口号(默认值1099)
#org.quartz.scheduler.rmi.registryport = 1099
#创建rmi注册,false/never:如果你已经有一个在运行或不想进行创建注册
# true/as_needed:第一次尝试使用现有的注册,然后再回来进行创建
# always:先进行创建一个注册,然后再使用回来使用注册
#org.quartz.scheduler.rmi.createregistry = never
#Quartz Scheduler服务端端口,默认是随机分配RMI注册表
#org.quartz.scheduler.rmi.serverport = 1098
#true:链接远程服务调度(客户端),这个也要指定registryhost和registryport,默认为false
# 如果export和proxy同时指定为true,则export的设置将被忽略
#org.quartz.scheduler.rmi.proxy = false
7.任务调度信息的存储之Mysql数据库
环境搭建:
由于我们使用了dbcp连接池和mysql数据库,所以要引入必需的连接池包和mysql驱动包:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
生成mysql数据库表的脚本,我使用的是quartz-1.8的版本对应的脚本
DROP TABLE IF EXISTS QRTZ_JOB_LISTENERS;
DROP TABLE IF EXISTS QRTZ_TRIGGER_LISTENERS;
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
CREATE TABLE QRTZ_JOB_DETAILS(
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_VOLATILE VARCHAR(1) NOT NULL,
IS_STATEFUL VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_JOB_LISTENERS (
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
JOB_LISTENER VARCHAR(200) NOT NULL,
PRIMARY KEY (JOB_NAME,JOB_GROUP,JOB_LISTENER),
INDEX (JOB_NAME, JOB_GROUP),
FOREIGN KEY (JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_TRIGGERS (
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
IS_VOLATILE VARCHAR(1) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),
INDEX (JOB_NAME, JOB_GROUP),
FOREIGN KEY (JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT(7) NOT NULL,
REPEAT_INTERVAL BIGINT(12) NOT NULL,
TIMES_TRIGGERED BIGINT(10) NOT NULL,
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),
INDEX (TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_CRON_TRIGGERS (
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),
INDEX (TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_BLOB_TRIGGERS (
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),
INDEX (TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_TRIGGER_LISTENERS (
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
TRIGGER_LISTENER VARCHAR(200) NOT NULL,
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_LISTENER),
INDEX (TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_CALENDARS (
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (CALENDAR_NAME))
ENGINE=InnoDB;
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_FIRED_TRIGGERS (
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
IS_VOLATILE VARCHAR(1) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_STATEFUL VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (ENTRY_ID))
ENGINE=InnoDB;
CREATE TABLE QRTZ_SCHEDULER_STATE (
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (INSTANCE_NAME))
ENGINE=InnoDB;
CREATE TABLE QRTZ_LOCKS (
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (LOCK_NAME))
ENGINE=InnoDB;
INSERT INTO QRTZ_LOCKS values('TRIGGER_ACCESS');
INSERT INTO QRTZ_LOCKS values('JOB_ACCESS');
INSERT INTO QRTZ_LOCKS values('CALENDAR_ACCESS');
INSERT INTO QRTZ_LOCKS values('STATE_ACCESS');
INSERT INTO QRTZ_LOCKS values('MISFIRE_ACCESS');
commit;
结果:
quartz.properties配置文件:
org.quartz.scheduler.instanceName:MyFirstScheduler
#线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class:org.quartz.simpl.SimpleThreadPool
#指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)
org.quartz.threadPool.threadCount:10
#设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority:5
org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix:QRTZ_
org.quartz.jobStore.dataSource:qzDS
org.quartz.dataSource.qzDS.driver:com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL:jdbc:mysql://localhost:3306/quartz
org.quartz.dataSource.qzDS.user:root
org.quartz.dataSource.qzDS.password:
org.quartz.dataSource.qzDS.maxConnections:10
#配置trigger执行历史日志(可以看到类的文档和参数列表)
org.quartz.plugin.triggHistory.class=org.quartz.plugins.history.LoggingTriggerHistoryPlugin
org.quartz.plugin.triggHistory.triggerFiredMessage=Trigger {1}.{0} fired job {6}.{5} at: {4, date, HH:mm:ss MM/dd/yyyy}
org.quartz.plugin.triggHistory.triggerCompleteMessage=Trigger {1}.{0} completed firing job {6}.{5} at {4, date, HH:mm:ss MM/dd/yyyy} with resulting trigger instruction code: {9}
让SimpleTrigger触发器的程序SimpleTriggerTest跑起来,如果我们中途突然停止了程序的运行,那么数据库就会存储当前组和触发器的程序状态。
SimpleTriggerTest程序:
public class SimpleTriggerTest {
public static void main(String[] args) {
try {
JobDetail jobDetail =new JobDetail("job1","jgroup1",MyTask.class);
SimpleTrigger simpleTrigger =new SimpleTrigger("trigger1","tgroup1");
simpleTrigger.setStartTime(new Date());
//每两秒执行一次
simpleTrigger.setRepeatInterval(2000);
//重复执行10次,那么就是一共执行101次了
simpleTrigger.setRepeatCount(100);
SchedulerFactory factory =new StdSchedulerFactory();
Scheduler scheduler =factory.getScheduler();
scheduler.scheduleJob(jobDetail, simpleTrigger);
scheduler.start();
} catch (SchedulerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
结果:
运行到出现第6条记录时,故意将程序停止了。那么对应的数据库记录的现场持久化的结果:
8.问题
如果我们停止程序后,想再次运行该程序就会发现,程序运行报异常:
显示的异常信息说:该组和任务不能进行存储了,因为它们本身已经存在了唯一的标识。
也证明了之前的结论:一个触发器只能触发一个Job任务。