1.场景
在电商系统中会经常遇到这样一种场景,就是商品的定时上下架功能,总不能每次都手动执行吧,这个时候我们首先想到的就是利用定时任务来实现这个功能。
目前实现定时任务主要有以下几种方式:
-
JDK自带 :JDK自带的Timer以及JDK1.5+ 新增的ScheduledExecutorService;
-
第三方框架 :使用 Quartz、elastic-job、xxl-job 等开源第三方定时任务框架,适合分布式项目应用。该方式的缺点是配置复杂。
-
Spring :使用 Spring 提供的一个注解
@Schedule
,开发简单,使用比较方便。
本文博主主要向大家介绍Quartz框架和Spring定时任务的使用。
2.什么是Quartz
Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。
Quartz 可以与 J2EE 与 J2SE 应用程序相结合也可以单独使用。
Quartz 允许程序开发人员根据时间的间隔来调度作业。
Quartz 实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。
3.Quartz几个核心概念
在正式学习使用Quartz之前,我们需要了解几个有关Quartz的核心概念,方便我们后面学习
-
Job 表示一个工作,要执行的具体内容。此接口中只有一个方法,如下:
void execute(JobExecutionContext context) // context是重要的上下文,可以访问到关联的JobDetail对象和本次触发的Trigger对象,以及在此之上设定的数据。
-
JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
-
Trigger 代表一个调度参数的配置,什么时候去调。
-
Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。
4.Quartz初体验
一、创建一个SpringBoot项目,pom.xml配置如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.songguoliang</groupId>
<artifactId>spring-boot-quartz</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring-boot-quartz</name>
<description>Spring Boot使用Quartz定时任务</description>
<!-- Spring Boot启动器父类 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring Boot web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
二、创建一个Job(Job里面是要执行的具体内容)
package com.example.quartz;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import java.time.LocalDateTime;
public class TestJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 通过context获取trigger中的数据
Object tv1 = context.getTrigger().getJobDataMap().get("t1");
Object tv2 = context.getTrigger().getJobDataMap().get("t2");
// 通过context获取JobDetail中的数据
Object jv1 = context.getJobDetail().getJobDataMap().get("j1");
Object jv2 = context.getJobDetail().getJobDataMap().get("j2");
Object sv = null;
try {
sv = context.getScheduler().getContext().get("skey");
} catch (SchedulerException e) {
e.printStackTrace();
}
System.out.println(tv1+":"+tv2);
System.out.println(jv1+":"+jv2);
System.out.println(sv);
System.out.println("date:"+ LocalDateTime.now());
}
}
三、执行Job
package com.example.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class QuartzTest {
public static void main(String[] args) {
try {
//创建一个scheduler
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//向scheduler中put值
scheduler.getContext().put("skey", "svalue");
//创建一个Trigger
Trigger trigger = TriggerBuilder.newTrigger()
//给该Trigger起一个id
.withIdentity("trigger1")
//以Key-Value形式关联数据
.usingJobData("t1", "tv1")
//每3秒触发一次,无限循环
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3)
.repeatForever()).build();
trigger.getJobDataMap().put("t2","tv2");
//创建一个JobDetail
JobDetail jobDetail = JobBuilder.newJob(TestJob.class)
//给该JobDetail起一个id
.withIdentity("myJob", "myGroup")
.usingJobData("j1", "jv1")
.build();
jobDetail.getJobDataMap().put("j2", "jv2");
//注册trigger并启动scheduler
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
//如果想要停止这个Job,可以调用shutdown方法
//scheduler.shutdown();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
控制台输出
10:46:54.075 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
10:46:54.079 [main] INFO org.quartz.simpl.SimpleThreadPool - Job execution threads will use class loader of thread: main
10:46:54.089 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
10:46:54.089 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
10:46:54.090 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
10:46:54.091 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
NOT STARTED.
Currently in standby mode.
Number of jobs executed: 0
Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.
10:46:54.091 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
10:46:54.091 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
10:46:54.104 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
10:46:54.104 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:46:54.106 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.example.quartz.TestJob
10:46:54.110 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:46:54.110 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob
tv1:tv2
jv1:jv2
svalue
date:2020-12-19T10:46:54.144
10:46:57.092 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.example.quartz.TestJob
10:46:57.092 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:46:57.092 [DefaultQuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob
tv1:tv2
jv1:jv2
svalue
date:2020-12-19T10:46:57.092
10:47:00.101 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.example.quartz.TestJob
10:47:00.101 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:47:00.101 [DefaultQuartzScheduler_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob
tv1:tv2
jv1:jv2
svalue
date:2020-12-19T10:47:00.101
10:47:03.096 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.example.quartz.TestJob
10:47:03.096 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:47:03.096 [DefaultQuartzScheduler_Worker-4] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob
tv1:tv2
jv1:jv2
svalue
date:2020-12-19T10:47:03.096
从输出结果我们可以看到此Job每隔3秒执行一次
有关概念
1、Job
job的一个 trigger 被触发后(稍后会讲到),execute() 方法会被 scheduler 的一个工作线程调用;传递给 execute() 方法的 JobExecutionContext 对象中保存着该 job 运行时的一些信息 ,执行 job 的 scheduler 的引用,触发 job 的 trigger 的引用,JobDetail 对象引用,以及一些其它信息。
2、JobDetail :
JobDetail 对象是在将 job 加入 scheduler 时,由客户端程序(你的程序)创建的。它包含 job 的各种属性设置,以及用于存储 job 实例状态信息的 JobDataMap
3、Trigger:
Trigger 用于触发 Job 的执行。当你准备调度一个 job 时,你创建一个 Trigger 的实例,然后设置调度相关的属性。Trigger 也有一个相关联的 JobDataMap,用于给 Job 传递一些触发相关的参数。Quartz 自带了各种不同类型的 Trigger,最常用的主要是 SimpleTrigger 和 CronTrigger。SimpleTrigger 主要用于一次性执行的 Job(只在某个特定的时间点执行一次),或者 Job 在特定的时间点执行,重复执行 N 次,每次执行间隔T个时间单位。CronTrigger 在基于日历的调度上非常有用,如“每个星期五的正午”,或者“每月的第十天的上午 10:15”等。
5.JobDetail详解
在定义一个Job时,我们需要实现Job接口,该接口只有一个execute
方法。
从上一节的案例中我们可以发现,我们通过Scheduler去执行Job,我们传给scheduler一个JobDetail实例,因为我们在创建JobDetail时,将要执行的job的类名传给了JobDetail,所以scheduler就知道了要执行何种类型的job。(这里利用了Java中的反射创建实例对象)每次当scheduler执行job时,在调用其execute(…)方法之前会创建该类的一个新的实例;执行完毕,对该实例的引用就被丢弃了,实例会被垃圾回收;这种执行策略带来的一个后果是,job必须有一个无参的构造函数(当使用默认的JobFactory时);另一个后果是,在job类中,不应该定义有状态的数据属性,因为在job的多次执行中,这些属性的值不会保留。
那么我们该如何给Job配置相关属性呢?答案就是通过JobDetail
JobDataMap
JobDataMap实现了Map接口,可以存放键值对数据,在Job执行的时候,我们就可以通过JobExecutionContext获取到JobDataMap中的数据,如下
JobDetail jobDetail = JobBuilder.newJob(TestJob.class)
.withIdentity("myJob", "myGroup")
.usingJobData("j1", "jv1")
.usingJobData("j2","jv2")
.build();
在job的执行过程中,可以从JobDataMap中取出数据,如下示例:
Object jv1 = context.getJobDetail().getJobDataMap().get("j1");
当然,如果你希望实现属性的自动注入,那么你可以使用下面的方法
package com.example.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class QuartzTest2 {
public static void main(String[] args) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1")
.usingJobData("t1", "tv1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3)
.repeatForever())
.build();
JobDetail jobDetail = JobBuilder.newJob(TestJob2.class)
.withIdentity("jd")
.usingJobData("name", "张三")
.usingJobData("age", 12)
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
package com.example.quartz;
import org.quartz.*;
public class TestJob2 implements Job {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobKey jobKey = context.getJobDetail().getKey();
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
System.out.println("name:" + name + "age:" +age);
}
}
给Job类加上get和set方法(属性名称要和JobDataMap中的key相同),那么JobDataMap中的值就是自动注入到Job中,不需要手动获取
6.Triggers详解
Trigger 用于触发 Job 的执行。当你准备调度一个 job 时,你创建一个 Trigger 的实例,然后设置调度相关的属性。所有类型的trigger都有TriggerKey这个属性,表示trigger的身份;除此之外,trigger还有很多其它的公共属性。这些属性,在构建trigger的时候可以通过TriggerBuilder设置。
triggers公共属性
- jobKey属性:当trigger触发时被执行的job的身份;
- startTime属性:设置trigger第一次触发的时间;该属性的值是java.util.Date类型,表示某个指定的时间点;有些类型的trigger,会在设置的startTime时立即触发,有些类型的trigger,表示其触发是在startTime之后开始生效。比如,现在是1月份,你设置了一个trigger–“在每个月的第5天执行”,然后你将startTime属性设置为4月1号,则该trigger第一次触发会是在几个月以后了(即4月5号)。
- endTime属性:表示trigger失效的时间点。比如,”每月第5天执行”的trigger,如果其endTime是7月1号,则其最后一次执行时间是6月5号。
优先级(priority)
如果你的trigger很多(或者Quartz线程池的工作线程太少),Quartz可能没有足够的资源同时触发所有的trigger;这种情况下,你可能希望控制哪些trigger优先使用Quartz的工作线程,要达到该目的,可以在trigger上设置priority属性。比如,你有N个trigger需要同时触发,但只有Z个工作线程,优先级最高的Z个trigger会被首先触发。如果没有为trigger设置优先级,trigger使用默认优先级,值为5;priority属性的值可以是任意整数,正数、负数都可以。
注意:只有同时触发的trigger之间才会比较优先级。10:59触发的trigger总是在11:00触发的trigger之前执行。
注意:如果trigger是可恢复的,在恢复后再调度时,优先级与原trigger是一样的。
错过触发(misfire Instructions)
trigger还有一个重要的属性misfire;如果scheduler关闭了,或者Quartz线程池中没有可用的线程来执行job,此时持久性的trigger就会错过(miss)其触发时间,即错过触发(misfire)。不同类型的trigger,有不同的misfire机制。它们默认都使用“智能机制(smart policy)”,即根据trigger的类型和配置动态调整行为
Simple Trigger
SimpleTrigger简单点说,就是在具体的时间点执行一次,或者在具体的时间点执行,并且以指定的间隔重复执行若干次。类似于闹钟,你定了一个周末早晨7点的闹钟,这个闹钟会在周末早上7点准时响起。闹钟还有个功能就是过5分钟之后再响一次,这对应着指定的间隔重复执行若干次。
1、指定时间开始触发,不重复:
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("st1", "group1")
.startAt(new Date()) // 从当前时间开始执行一次,不重复
.build();
2、指定时间触发,每隔2秒执行一次,重复5次:
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("st2", "group1")
.startAt(new Date())
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(2) // 2秒
.withRepeatCount(5)// 5次
)
.build();
3、1分钟以后开始触发,仅执行一次:
long time = 1 * 60 * 1000;
Date now = new Date();
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("st3", "group1")
.startAt(new Date(now.getTime() + time))
.build();
4、立即触发,每隔2秒钟执行一次,直到2020-12-19 13:20:00
String dateStr="2020-12-19 13:20:00";
String pattern="yyyy-MM-dd HH:mm:ss";
SimpleDateFormat dateFormat=new SimpleDateFormat(pattern);
Date date = dateFormat.parse(dateStr);
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("st4", "group1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.endAt(date)
.build();
5、在13:00触发,然后每2小时重复一次:
String dateStr="2020-12-19 13:00:00";
String pattern="yyyy-MM-dd HH:mm:ss";
SimpleDateFormat dateFormat=new SimpleDateFormat(pattern);
Date date = dateFormat.parse(dateStr);
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("st2", "group1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInHours(2)
.repeatForever())
.build();
SimpleTrigger Misfire
misfire:被错过的执行任务策略
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("st6")
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(5)
.repeatForever()
.withMisfireHandlingInstructionNextWithExistingCount()
)
.build();
CronTrigger
CronTrigger通常比Simple Trigger更有用,如果你需要在指定日期执行某项任务,使用CronTrigger就非常方便,比如如果你想在每月的15号给会员发放优惠券,或者每周五中午12点统计用户本周使用产品时长。
cron
表达式是一个字符串,该字符串由 6 个空格分为 7 个域,每一个域代表一个时间含义。 通常定义 “年” 的部分可以省略,实际常用的 Cron 表达式由前 6 部分组成。格式如下
[秒] [分] [时] [日] [月] [周] [年]
Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field)
域 | 是否必填 | 值以及范围 | 通配符 |
---|---|---|---|
秒 | 是 | 0-59 | , – * / |
分 | 是 | 0-59 | , – * / |
时 | 是 | 0-23 | , – * / |
日 | 是 | 1-31 | , – * ? / L W |
月 | 是 | 1-12 或 JAN-DEC | , – * / |
周 | 是 | 1-7 或 SUN-SAT | , – * ? / L # |
年 | 否 | 1970-2099 | , – * / |
需要说明的是,Cron 表达式中,“周” 是从周日开始计算的。“周” 域上的 1
表示的是周日,7
表示周六。
每天晚上12点触发任务:
0 0 0 * * ?
每隔 1 分钟执行一次:
0 */1 * * * ?
每月 1 号凌晨 1 点执行一次:
0 0 1 1 * ?
每月最后一天 23 点执行一次:
0 0 23 L * ?
每周周六凌晨 3 点实行一次:
0 0 3 ? * L
在24分,30分执行一次:
0 24,30 * * * ?
是不是有点没看懂,没关系,我们可以使用Cron表达式生成器帮助我们生成Cron表达式
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/2 8-17 * * ?"))
.build();
7.@Schedule实现定时任务
很多时候我们都需要为系统建立一个定时任务来帮我们做一些事情,SpringBoot 已经帮我们实现好了一个,我们只需要直接使用即可
一、引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
二、开启注解
在 SpringBoot 中我们只需要在启动类上加上@EnableScheduling
便可以启动定时任务了。
@SpringBootApplication
@EnableScheduling
public class TaskApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
三、创建scheduled task
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author wugongzi
*/
@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
/**
* fixedRate:固定速率执行。每5秒执行一次。
*/
@Scheduled(fixedRate = 5000)
public void reportCurrentTimeWithFixedRate() {
log.info("Current Thread : {}", Thread.currentThread().getName());
log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date()));
}
/**
* fixedDelay:固定延迟执行。距离上一次调用成功后2秒才执。
*/
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
try {
TimeUnit.SECONDS.sleep(3);
log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* initialDelay:初始延迟。任务的第一次执行将延迟5秒,然后将以5秒的固定间隔执行。
*/
@Scheduled(initialDelay = 5000, fixedRate = 5000)
public void reportCurrentTimeWithInitialDelay() {
log.info("Fixed Rate Task with Initial Delay : The time is now {}", dateFormat.format(new Date()));
}
/**
* cron:使用Cron表达式。 每分钟的1,2秒运行
*/
@Scheduled(cron = "1-2 * * * * ? ")
public void reportCurrentTimeWithCronExpression() {
log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
}
}
启动项目便可以看到效果。