Quertz是一个开源的作业任务调度框架,他可以完成像JavaScript定时器类式的功能,其实Java中Timer也可实现部分功能,但相比Quertz还是略逊一筹。
Quartz 核心元素
(1)添加依赖
org.quartz-scheduler
quartz
2.2.1
org.quartz-scheduler
quartz-jobs
2.2.1
(2)自定义任务工厂
覆盖SpringBeanJobFactory中的createJobInstance(), 对其创建出来的类自动注入
(可以嵌在配置类作为静态内部类,然后实例化对象赋值)
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
import org.springframework.stereotype.Component;
//继承SpringBeanJobFactory 实现任务实例化方式
//ApplicationContextAware 通过它Spring容器会自动把上下文环境对象调用ApplicationContextAware接口中的setApplicationContext方法
@Component //把普通pojo实例化到spring容器中
public class CustomJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;
//将job实例交给spring ioc容器管理
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
}
(3)添加配置类
定义configuration并暴露bean给spring IOC
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
@Configuration
@EnableScheduling //开启Spring 的schedule task执行的能力,一般作用于配置类
public class SchedulerConfiguration {
@Autowired
private ApplicationContext applicationContext;
// 配置文件路径
static final String QUARTZ_CONFIG = "/quartz.properties";
@Autowired
private CustomJobFactory customJobFactory ;
@Bean
public JobFactory jobFactory(ApplicationContext applicationContext) {
//自定义工厂整合ApplicationContext
//return new CustomJobFactory (),
//可省略下面的代码,因为spring加载时自动调用setApplicationContext
CustomJobFactory jobFactory = new CustomJobFactory ();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}
//配置任务调度器
@Bean //注解的方法参数默认@Autowired
public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource)
throws Exception {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
// 将自定义job工厂交给调度器来维护
schedulerFactoryBean.setJobFactory(jobFactory);
//用于Quartz集群,启动时更新(覆盖)已存在的Job
schedulerFactoryBean.setOverwriteExistingJobs(true);
// 项目启动完成后,等待2秒后开始执行调度器初始化
schedulerFactoryBean.setStartupDelay(2);
// 设置调度器自动运行
schedulerFactoryBean.setAutoStartup(true);
// 设置数据源,使用与项目统一数据源
schedulerFactoryBean.setDataSource(dataSource);
// 设置上下文spring bean name
schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
// 设置配置文件位置
schedulerFactoryBean.setConfigLocation(new ClassPathResource(QUARTZ_CONFIG));
return schedulerFactoryBean;
}
}
(4)Quartz 对于定时任务,提供了一个Job接口。所有你需要实现的定时任务都需要实现Job接口。
/**
*job执行入口
**/
public class BaseJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//do something you want to do
System.out.println("job 1 start!!!");
}
}
public class TestJob implements BaseJob {
public TestJob() {
}
private void initService() {
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// JobDetail中的JobDataMap是共用的,从getMergedJobDataMap获取的JobDataMap是全新的对象
// JobDataMap map = context.getMergedJobDataMap();
initService();
JobDataMap map = context.getJobDetail().getJobDataMap();
try {
map.get("params").toString();
//TODO
} catch (Exception e1) {
logger.info("{}", e1);
e1.printStackTrace();
}
}
}
(5)任务基本属性,可构建实体类并持久化到数据库中。
String jobName; //任务名称
String jobGroupName; //任务组名称
String triggerName; //触发器名称
String triggerGroupName; //触发器组名称
String jobStatus; //任务状态
String cronExpression; //任务表达式
String createTime; //创建时间
String classPath; //任务类类型
String params; //job参数
String description; //任务描述
(6)定时任务service实现
@Autowired
private Scheduler scheduler;
/**
* 功能: 添加一个定时任务
* @param jobName 任务名(同一个任务组中唯一)
* @param jobGroupName 任务组名
* @param triggerName 触发器名(同一个触发器组中唯一)
* @param triggerGroupName 触发器组名
* @param jobClass 任务的类类型 eg:TimedMassJob.class
* @param cron 时间设置 表达式,参考quartz说明文档
* @param objects 可变参数需要进行传参的值
*/
public void addjob(String jobName, String jobGroupName, String triggerName,String triggerGroupName, Class jobClass, String cron, Object... objects){
try {
//job实例属性,通过JobBuilder类创建
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName).build();
if(objects != null){
Integer i = 0;
for (Object obj: objects) {
//JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,
//可以使用其中的数据;JobDataMap是Java Map接口的一个实现,
//额外增加了一些便于存取基本类型的数据的方法。
jobDetail.getJobDataMap().put("data"+i, obj);
i++;
}
}
//TriggerBuilder是一个建造者模式,链式建造。通过静态方法构建一个TriggerBuilder实例,
//然后再调用类方法Build()创建一个ITrigger的实现。
TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger();
triggerBuilder.withIdentity(triggerName, triggerGroupName);
triggerBuilder.startNow();
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
//CronTrigger通常比Simple Trigger更有用,如果您需要基于日历的概念而不是按照SimpleTrigger的精确指定间隔进行重新启动的作业启动计划。
CronTrigger cronTrigger = (CronTrigger)triggerBuilder.build();
scheduler.scheduleJob(jobDetail, cronTrigger);
if(!scheduler.isShutdown()){
scheduler.start();
}
} catch (SchedulerException e) {
logger.error("error:\n",e);
}
}
/**
* 功能: 移除一个任务
* @param jobName
* @param jobGroupName
* @param triggerName
* @param triggerGroupName
*/
public void removeJob(String jobName, String jobGroupName,String triggerName, String triggerGroupName){
try {
//Trigger身份对象
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
//停止触发器
scheduler.pauseTrigger(triggerKey);
//移除触发器
scheduler.unscheduleJob(triggerKey);
//删除任务
//JobKey:Job身份对象
scheduler.deleteJob(JobKey.jobKey(jobName,jobGroupName));
} catch (SchedulerException e) {
logger.error("error:\n",e);
}
}
/**
* 功能:修改一个任务的触发时间
* @param triggerName 触发器名
* @param triggerGroupName 触发器组名
* @param cron 时间设置,参考quartz说明文档
*/
public void modifyJobTime(String triggerName,String triggerGroupName, String cron){
try {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
CronTrigger trigger = (CronTrigger)scheduler.getTrigger(triggerKey);
if(trigger == null){
return;
}
String oldTime = trigger.getCronExpression();
if(!oldTime.equalsIgnoreCase(cron)){
TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger();
triggerBuilder.withIdentity(triggerName, triggerGroupName);
triggerBuilder.startNow();
//CronTrigger使用Cron表达是来定义任务的触发时间,CronTrigger由CronScheduleBuilder构建而成
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
trigger = (CronTrigger) triggerBuilder.build();
scheduler.rescheduleJob(triggerKey, trigger);
}
} catch (SchedulerException e) {
logger.error("error:\n",e);
}
}
/**
* 暂停定时任务
*
* @param jobName
* @param jobGroup
*/
@Override
public void pause(String jobName, String jobGroup) {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
try {
if (checkExists(jobName, jobGroup)) {
scheduler.pauseTrigger(triggerKey);
logger.info("pause job success, triggerKey:{},jobGroup:{}, jobName:{}", triggerKey, jobGroup, jobName);
}
} catch (SchedulerException e) {
logger.error(e.getMessage());
}
}
/**
* 重新开始任务
*
* @param jobName
* @param jobGroup
*/
@Override
public void resume(String jobName, String jobGroup) {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
try {
if (checkExists(jobName, jobGroup)) {
scheduler.resumeTrigger(triggerKey);
logger.info("resume job success,triggerKey:{},jobGroup:{}, jobName:{}", triggerKey, jobGroup, jobName);
}
} catch (SchedulerException e) {
logger.error(e.getMessage());
}
}
/**
* 验证任务是否存在
*
* @param jobName
* @param jobGroup
* @throws SchedulerException
*/
@Override
public boolean checkExists(String jobName, String jobGroup) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
return scheduler.checkExists(triggerKey);
}
/**
* 获取JobDetail
*/
@Override
public JobDetail geJobDetail(JobKey jobKey, String className, String description, JobDataMap map)
throws Exception {
return JobBuilder.newJob(getClass(className)).withIdentity(jobKey).withDescription(description)
.setJobData(map).storeDurably().build();
}
/**
* 获取Trigger (Job的触发器,执行规则) (所有misfire的任务会马上执行)
*/
@Override
public Trigger getTrigger(SysJob job) {
return TriggerBuilder.newTrigger().withIdentity(job.getCronName(), job.getGroupName())
.withSchedule(CronScheduleBuilder.cronSchedule(job.getCron())
.withMisfireHandlingInstructionFireAndProceed())
.build();
}
/**
* 启动所有任务
**/
public void startJobs(){
try {
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
} catch (SchedulerException e) {
logger.error("error:\n",e);
}
}
/**
* 关闭所有任务
**/
public void shutdownJobs(){
try {
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.shutdown();
} catch (SchedulerException e) {
logger.error("error:\n",e);
}
}
(7)工具类
public class JobCronUtil {
/**
* 根据执行时间,创建quarz cron表达式
* @param jobTime 任务执行时间
*/
public static String createCron(Date jobTime) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(jobTime);
StringBuffer cronBuff = new StringBuffer();
cronBuff.append(calendar.get(Calendar.SECOND)).append(" ");
cronBuff.append(calendar.get(Calendar.MINUTE)).append(" ");
cronBuff.append(calendar.get(Calendar.HOUR_OF_DAY)).append(" ");
cronBuff.append(calendar.get(Calendar.DAY_OF_MONTH)).append(" ");
cronBuff.append(calendar.get(Calendar.MONTH) + 1).append(" ");
cronBuff.append(" ? ");
//cronBuff.append(calendar.get(Calendar.YEAR)+"-"+calendar.get(Calendar.YEAR)).append(" ");
return cronBuff.toString();
}
/**
* 根据执行时间、时间间隔,创建 quarz cron表达式
* @param jobBeginTime 任务开始时间
* @param interval 间隔长度
* @param intervalUnit 间隔单位
*/
public static String createCron(Date jobBeginTime, Integer interval, TimeUnit intervalUnit) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(jobBeginTime);
StringBuffer cronBuff = new StringBuffer();
cronBuff.append(calendar.get(Calendar.SECOND)).append(" ");
cronBuff.append(calendar.get(Calendar.MINUTE));
if (interval != null && TimeUnit.minute.equals(intervalUnit)) {
cronBuff.append("/" + interval);
}
cronBuff.append(" ").append(calendar.get(Calendar.HOUR_OF_DAY));
if (interval != null && TimeUnit.hour.equals(intervalUnit)) {
cronBuff.append("/" + interval);
}
cronBuff.append(" ").append(calendar.get(Calendar.DAY_OF_MONTH));
if (interval != null && TimeUnit.day.equals(intervalUnit)) {
cronBuff.append("/" + interval);
}
cronBuff.append(" ").append(calendar.get(Calendar.MONTH) + 1).append(" ");
cronBuff.append(" ? ");
//cronBuff.append(calendar.get(Calendar.YEAR)+"-"+calendar.get(Calendar.YEAR)).append(" ");
return cronBuff.toString();
}
}
(8)quartz.properties
#quartz集群配置
# ===========================================================================
# Configure Main Scheduler Properties 调度器属性
# ===========================================================================
#调度标识名 集群中每一个实例都必须使用相同的名称
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
#ID设置为自动获取 每一个必须不同
org.quartz.scheduler.instanceid=AUTO
#禁用quartz软件更新
org.quartz.scheduler.skipUpdateCheck=true
#============================================================================
# Configure ThreadPool
#============================================================================
#线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
#指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)
org.quartz.threadPool.threadCount = 25
#设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority = 5
#============================================================================
# Configure JobStore
#============================================================================
# 信息保存时间 默认值60秒
org.quartz.jobStore.misfireThreshold = 60000
#数据保存方式为数据库持久化
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#JobDataMaps是否都为String类型
org.quartz.jobStore.useProperties = false
#数据库别名 随便取
org.quartz.jobStore.dataSource = myDS
#表的前缀,默认QRTZ_
org.quartz.jobStore.tablePrefix = QRTZ_
#是否加入集群
org.quartz.jobStore.isClustered = true
#调度实例失效的检查时间间隔
org.quartz.jobStore.clusterCheckinInterval = 20000
#============================================================================
# Configure Datasources
#============================================================================
#自定义连接池
org.quartz.dataSource.myDS.connectionProvider.class=cn.lsr.quartz.config.DruidConnectionProvider
#数据库引擎
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
#数据库连接
org.quartz.dataSource.myDS.URL = jdbc:mysql://127.0.0.1:3306/lsr-microservice?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
#数据库用户
org.quartz.dataSource.myDS.user = root
#数据库密码
org.quartz.dataSource.myDS.password = lishirui
#允许最大连接
org.quartz.dataSource.myDS.maxConnections = 5
#验证查询sql,可以不设置
org.quartz.dataSource.myDS.validationQuery=select 0 from dual
#注:如果嫌需要额外配置quart数据源很烦,也可以共用你项目配置的数据库链接,这样每次更换数据库连接,就不需要额外在修改。
(9)cron表达式
Quartz里面有一个cron表达式,用来设置定时任务执行的时间
1.Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:
Seconds Minutes Hours DayofMonth Month DayofWeek Year或 Seconds Minutes Hours DayofMonth Month DayofWeek
2.每一个域可出现的字符如下:
Seconds:可出现", - * /"四个字符,有效范围为0-59的整数
Minutes:可出现", - * /"四个字符,有效范围为0-59的整数
Hours:可出现", - * /"四个字符,有效范围为0-23的整数
DayofMonth:可出现", - * / ? L W C"八个字符,有效范围为0-31的整数
Month:可出现", - * /"四个字符,有效范围为1-12的整数或JAN-DEc
DayofWeek:可出现", - * / ? L C #"四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推
Year:可出现", - * /"四个字符,有效范围为1970-2099年
每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:
(1)*:表示匹配该域的任意值,假如在Minutes域使用*, 即表示每分钟都会触发事件。
(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。
例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?,
其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。
(3)-: 表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次 。
(4)/:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.。
(5),: 表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
(6)L: 表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
(7)W: 表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在
DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;
如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份。
(8)LW: 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
(9)#: 用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。
举几个例子:
0 0 2 1 * ? * 表示在每月的1日的凌晨2点调度任务
0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。
按顺序依次为
秒(0~59)
分钟(0~59)
小时(0~23)
天(月)(0~31,但是你需要考虑你月的天数)
月(0~11)
天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)
年份(1970-2099
其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置?
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 12 ? * WED 表示每个星期三中午12点
"0 0 12 * * ?" 每天中午12点触发
"0 15 10 ? * *" 每天上午10:15触发
"0 15 10 * * ?" 每天上午10:15触发
"0 15 10 * * ? *" 每天上午10:15触发
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2: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 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
3. 有些子表达式能包含一些范围或列表。
例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”
“*”字符代表所有可能的值
因此,“*”在子表达式(月)里表示每个月的含义,“*”在子表达式(天(星期))表示星期的每一天
4. “/”字符用来指定数值的增量
例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟
在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样
5. “?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值
当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”
“L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写,但是它在两个子表达式里的含义是不同的。
在天(月)子表达式中,“L”表示一个月的最后一天
在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT
如果在“L”前有具体的内容,它就具有其他的含义了
例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五
注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题。
参考:GO GO GO