Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,作用是用来执行任务调度。然而任务调度本身涉及多线程并发、运行舒缓规则制定及解析、运行现场保持与恢复、以及线程池维护等很多工作。如果我们自己去开发,难度很大,而quartz工具为我们提供了便利。可以让我们创建多个Job任务。
引入maven的jar包:
<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>1.8.6</version> </dependency>
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类使用一个线程池作为任务运行的基础设施。任务通过共享线程池中的线程来提高运行效率。
实例:
任务类:
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(); } } } 结果:
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条记录时,故意将程序停止了。那么对应的数据库记录的现场持久化的结果:
如果我们停止程序后,想再次运行该程序就会发现,程序运行报异常:
显示的异常信息说:该组和任务不能进行存储了,因为它们本身已经存在了唯一的标识。
也证明了之前的结论:一个触发器只能触发一个Job任务。