JAVA 针对定时任务,有 Timer,Scheduler, Quartz 等几种实现方式,其中最常用的应该就是 Quartz 了。
一. Quartz的基本概念
在开始之前,我们必须了解以下的几个基本概念:Job、Trigger、Cron expression、JobStores
1.1 Job
对于任务内容的构建,我们需要创建 Job 的继承类,并实现 execute 方法。
1.2 Trigger
其作用是设置调度策略。Quartz 设计了多种类型的 Trigger,其中最常用的是 SimpleTrigger 和 CronTrigger。
- SimpleTrigger 适用于在某一特定的时间以某一特定时间间隔执行多次。
- CronTrigger 的用途更广,主要适用于基于日历的调度安排。例如:每星期二的 16:38:10 执行,每月一号执行,等等。本文使用的是CronTrigger。
1.3 Cron表达式
由七个字段组成:
Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (Optional field)
例如:创建一个每三小时执行的 CronTrigger,且从每小时的整点开始执行:
/ * * ?
Cron在线生成工具:http://cron.qqe2.com
1.4 JobStores
负责对Quartz的持久化,即将任务调度的相关数据保存在数据库中。这样,在系统重启后,任务的调度状态依然存在系统中。当任务错过了触发时间时,还可以重新触发并执行(需要配置)。
二、新建 Quartz 工程
我们会新建一个 Maven 工程,并在启动Web应用之后运行定时任务。
2.1 新建工程
在 Eclipse 中,新建 Maven 项目并引入 Jar 包
2.2 版本信息
Java | 1.8 |
Tomcat | 8.5 |
Spring | 4.3.3.RELEASE |
Quartz | 2.2.1 |
2.3 工程结构
2.4 Spring Quartz 的配置
主要是定义了数据源,调度工厂以及定时任务的配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd" default-autowire="no" > <!-- 读取属性文件 --> <context:property-placeholder location="classpath*:properties/quartz.properties" ignore-unresolvable="true" /> <!-- 扫描包 --> <context:component-scan base-package="org.stevexie" /> <!-- 数据源 --> <bean id="quartzDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>${jdbc.driverclass}</value> </property> <property name="url"> <value>${jdbc.url}</value> </property> <property name="username"> <value>${jdbc.username}</value> </property> <property name="password"> <value>${jdbc.password}</value> </property> </bean> <!-- ======================== 调度工厂开始 ======================== --> <bean id="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" lazy-init="false" autowire="no"> <!-- 设置持久化的数据源 --> <property name="dataSource"> <ref bean="quartzDataSource" /> </property> <!-- 设置Quartz的属性 --> <property name="quartzProperties"> <props> <!-- 线程池配置 --> <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop> <!-- 初始化线程数为20 --> <prop key="org.quartz.threadPool.threadCount">20</prop> <!-- 设置作业的优先级是5 --> <prop key="org.quartz.threadPool.threadPriority">5</prop> <!-- 初始化线程数为20 --> <prop key="org.quartz.jobStore.misfireThreshold">60000</prop> <!-- JobStore 配置,通过数据库存储最终调度程序的配置 --> <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop> <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop> <!-- 数据表名的前缀设置 --> <prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop> </props> </property> <!-- 设置应用初始化之后,延迟30秒再启动scheduler --> <property name="startupDelay" value="30" /> <property name="applicationContextSchedulerContextKey" value="applicationContext" /> <!-- 设置定时任务随web启动 --> <property name="autoStartup" value="true" /> <property name="triggers"> <list> <ref bean="definedInXMLcronTrigger"/> </list> </property> </bean> <!-- ======================== 调度工厂结束 ======================== --> <!-- ======================== 定时任务1 开始 ======================== --> <bean id="definedInXMLJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean"> <!-- 必填项:在此处定义job detail --> <property name="jobClass" value="org.stevexie.jobdetail.DefinedInXMLJobDetail" /> <!-- 必填项:在此处定义job name --> <property name="name" value="jobName1"></property> <!-- 必填项:在此处定义job group name --> <property name="group" value="jobGroupName1"></property> <!-- 选填项:设置该job是持久性的 --> <property name="durability" value="true" /> <!-- 选填项:设置该job是中断后可恢复的 --> <property name="requestsRecovery" value="true" /> </bean> <bean id="definedInXMLcronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <!-- 必填项:在此处定义trigger name --> <property name="name" value="triggerName1" /> <!-- 必填项:在此处定义trigger group name --> <property name="group" value="triggerGroupName1" /> <property name="jobDetail" ref="definedInXMLJobDetail" /> <!-- 必填项:在此处定义定时任务时间 --> <property name="cronExpression" value="0/10 * * * * ? " /> <!-- 选填项:在此处定义市区 --> <property name="timeZone" ref="timeZone" /> <property name="misfireInstruction" value="1" /> </bean> <!-- ======================== 定时任务1 结束 ======================== --> <!-- 时区 --> <bean id="timeZone" class="java.util.TimeZone" factory-method="getTimeZone"> <constructor-arg value="GMT+08:00" /> </bean> </beans>
2.5 定时任务
实现两个定时任务:一个是在应用初始化的时候启动;另一个是通过运行代码中的main方法来实现对定时任务的增删改。
2.5.1 定时任务1
随着应用的启动而实例化,每10秒打印一条语句:
package org.stevexie.jobdetail; import org.quartz.JobExecutionContext; import org.springframework.scheduling.quartz.QuartzJobBean; import org.stevexie.util.DateUtil; public class DefinedInXMLJobDetail extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context) { System.out.println("执行在XML中定义好的定时任务,当前时间是:" + DateUtil.currentDatetime()); } }
2.5.2 定时任务2
与定时任务1的内容基本一致,但主要是在 JAVA 代码中被实例化并实现定时任务的增删改。
package org.stevexie.jobdetail; import org.quartz.JobExecutionContext; import org.springframework.scheduling.quartz.QuartzJobBean; import org.stevexie.util.DateUtil; public class DefinedInCodeJobDetail extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context) { System.out.println("执行在代码中添加的定时任务,当前时间是:" + DateUtil.currentDatetime()); } }
2.6 定时任务的增删改工具类
package org.stevexie.util; import java.text.ParseException; import javax.annotation.Resource; import org.apache.commons.lang.StringUtils; import org.quartz.CronScheduleBuilder; import org.quartz.CronTrigger; import org.quartz.Job; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.stereotype.Component; import org.stevexie.jobdetail.DefinedInCodeJobDetail; import org.stevexie.jobdetail.DefinedInXMLJobDetail; @Component("quartzUtil") public class QuartzUtil { @Resource(name="quartzScheduler") private Scheduler scheduler; private static String JOB_GROUP_NAME = "ddlib"; private static String TRIGGER_GROUP_NAME = "ddlibTrigger"; /** * 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名 * @param job Job任务类实例 * @param jobName job名字(请保证唯一性) * @param cronExpression cron时间表达式 * @throws SchedulerException */ public void addJob(String jobName, Job job, String cronExpression) throws SchedulerException, ParseException{ addJob(job, jobName, null, jobName, null, cronExpression, 5); } /** * 开始一个simpleSchedule()调度(创建一个新的定时任务) * @param job Job任务类实例 * @param jobName job名字(请保证唯一性) * @param jobGroupName job组名(请保证唯一性) * @param cronExpression cron时间表达式 * @param triggerName trigger名字(请保证唯一性) * @param triggerGroupName triggerjob组名(请保证唯一性) * @throws SchedulerException */ public void addJob(Job job, String jobName, String jobGroupName, String triggerName, String triggerGroupName, String cronExpression, int triggerPriority) throws SchedulerException { if(StringUtils.isEmpty(jobGroupName)){ jobGroupName = JOB_GROUP_NAME; } if(StringUtils.isEmpty(triggerGroupName)){ triggerGroupName = TRIGGER_GROUP_NAME; } // 1、创建一个JobDetail实例,指定Quartz JobDetail jobDetail = JobBuilder.newJob(job.getClass()) // 任务执行类 .withIdentity(jobName, jobGroupName) // 任务名,任务组 .build(); // 2、创建Trigger // 优先级默认是5,数字越高优先级越高 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity(triggerName, triggerGroupName).startNow() .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) .withPriority(triggerPriority) .build(); // 3、移除job,避免因为job的重复添加导致错误 this.removeJob(jobName, jobGroupName, triggerName, triggerGroupName); // 4、调度执行 try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { e.printStackTrace(); throw e; } // 4、启动 this.startSchedule(); } /** * 开始任务 * @throws SchedulerException */ public void startSchedule() throws SchedulerException { try { if(scheduler.isShutdown()){ scheduler.resumeAll(); } else { scheduler.start(); } } catch (SchedulerException e) { e.printStackTrace(); throw e; } } /** * 暂停Job * @param name job名字 * @param group job组名 * @throws SchedulerException */ public void pauseJob(String jobName, String jobGroupName) throws SchedulerException { JobKey jobKey = JobKey.jobKey(jobName, jobGroupName); try { scheduler.pauseJob(jobKey); } catch (SchedulerException e) { e.printStackTrace(); throw e; } } /** * 恢复Job * @param name job名字 * @param group job组名 * @throws SchedulerException */ public void resumeJob(String jobName, String jobGroupName) throws SchedulerException { JobKey jobKey = JobKey.jobKey(jobName, jobGroupName); try { scheduler.resumeJob(jobKey); } catch (SchedulerException e) { e.printStackTrace(); throw e; } } /** * 修改一个任务的触发时间(使用默认的任务组名,触发器名,触发器组名) */ public void modifyJobTime(String jobName, String cronExpression) throws SchedulerException, ParseException { rescheduleJob(jobName, null, cronExpression); } /** * 更新任务表达式 * @param triggerName trigger名字 * @param triggerGroupName trigger组名 * @param newCronExpression cron时间表达式 * @throws SchedulerException */ public void rescheduleJob( String triggerName, String triggerGroupName, String newCronExpression) throws SchedulerException { if(StringUtils.isBlank(triggerGroupName)) { triggerGroupName = TRIGGER_GROUP_NAME; } TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName); // 获取trigger CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); // 按新的cronExpression表达式重新构建trigger trigger = trigger .getTriggerBuilder() .withIdentity(triggerKey) .withSchedule(CronScheduleBuilder.cronSchedule(newCronExpression)) .build(); // 按新的trigger重新设置job执行 try { scheduler.rescheduleJob(triggerKey, trigger); } catch (SchedulerException e) { e.printStackTrace(); throw e; } } /** 移除一个任务和触发器(使用默认的任务组名,触发器名,触发器组名) */ public void removeJob(String jobName,String triggerName) throws SchedulerException{ removeJob(jobName, null, triggerName, null); } /** 移除一个任务和触发器 */ public void removeJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName) throws SchedulerException { if(StringUtils.isEmpty(jobGroupName)) { jobGroupName = JOB_GROUP_NAME; } if(StringUtils.isEmpty(triggerGroupName)) { triggerGroupName = TRIGGER_GROUP_NAME; } JobKey jobKey = JobKey.jobKey(jobName, jobGroupName); TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName); scheduler.pauseTrigger(triggerKey); // 停止触发器 scheduler.unscheduleJob(triggerKey); // 移除触发器 scheduler.deleteJob(jobKey); // 删除任务 } public static void main(String[] args) { ApplicationContext context = null; context = new ClassPathXmlApplicationContext("classpath:spring/spring-quartz.xml"); QuartzUtil quartzUtil = (QuartzUtil) context.getBean("quartzUtil"); // 删除定时器 try { quartzUtil.removeJob( "jobName", "jobGroupName", "triggerName", "triggerGroupName"); quartzUtil.removeJob( "jobName1", "jobGroupName", "triggerName", "triggerGroupName"); quartzUtil.removeJob( "jobName2", "jobGroupName", "triggerName", "triggerGroupName"); } catch (SchedulerException e1) { e1.printStackTrace(); } // 添加定时器 try { quartzUtil.addJob(new DefinedInXMLJobDetail(), "jobName1", "jobGroupName1", "triggerName1", "triggerGroupName1", "0/20 * * * * ? ", 5); quartzUtil.addJob(new DefinedInCodeJobDetail(), "jobName2", "jobGroupName2", "triggerName2", "triggerGroupName2", "0/10 * * * * ? ", 5); } catch (SchedulerException e2) { e2.printStackTrace(); } // 修改定时器 try { quartzUtil.rescheduleJob( "triggerName1", "triggerGroupName1", "0/30 * * * * ? "); } catch (SchedulerException e1) { e1.printStackTrace(); } try { Thread.sleep(100*1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
2.7. Quartz的执行脚本
需要在数据库中建一些Quartz的表。
# # Quartz seems -bin.jar # # PLEASE consider using mysql with innodb tables to avoid locking issues # # In your Quartz properties file, you'll need to set # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 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_SIMPROP_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 ( SCHED_NAME VARCHAR(120) NOT NULL, 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_NONCONCURRENT VARCHAR(1) NOT NULL, IS_UPDATE_DATA VARCHAR(1) NOT NULL, REQUESTS_RECOVERY VARCHAR(1) NOT NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) 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 (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, 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 (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, CRON_EXPRESSION VARCHAR(200) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, BLOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(200) NOT NULL, CALENDAR BLOB NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, FIRED_TIME BIGINT(13) NOT NULL, SCHED_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_NONCONCURRENT VARCHAR(1) NULL, REQUESTS_RECOVERY VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, LAST_CHECKIN_TIME BIGINT(13) NOT NULL, CHECKIN_INTERVAL BIGINT(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) ); commit;
三、 运行效果
3.1 初次运行web项目
每十秒钟会执行一次定时任务。该任务是在XML中被定义的,随着Web应用的启动而实例化。
3.2 修改定时任务的属性
停止 Web 项目,运行 QuartzUtil.java。结果显示,目前有两个定时任务正在运行:
- 定时任务一,每隔30秒运行一次,并打印:执行在XML中定义好的定时任务...
- 定时任务二,每隔10秒运行一次,并打印:执行在代码中添加的定时任务...
3.3 再次运行 Web 项目,确认修改结果
执行结果与 3.2 一致,说明定时任务的触发时间已经成功被修改了: