Qquartz定时任务
一、任务调度概念
1.1 什么是任务调度
任务调度是为了自动完成特定任务,在约定时刻去执行任务的过程。
如:
- 每个月初对用户进行缴费提醒
- 每天定时12:00给用户发放优惠券
1.2 为什么要使用分布式调度
在spring中提供了注解@Scheduled
,也能够实现任务调度的功能。具体步骤:
- 在业务类的方法上加上注解,写上cron表达式:
@Scheduled(cron = "0/20 * * * * ?")
public void method(){
//do task
}
- 在启动类上加上@EnableScheduling注解开启顶定时调度功能
spring提供了单机版的定时任务调度,那为什么还要使用分布式定时任务Quartz呢?
- 高可用:
单机版的定时任务调度只能在一台机器上运行,如果程序或者系统出现异常就会导致功能不可用。 - 方式重复执行:
当我们部署了多台服务器,同时又每台都有定时任务,若不进行合理的控制确保只有一个定时任务启动执行,定时执行的结果就会存在混乱和错误。(如积分的重复累加) - 单机处理极限:
单机能力有限(CPU、内存和磁盘),会存在单机处理不过来的情况
二、Quartz快速上手
- 引入包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
- 测试案例
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.JobBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
public class QuartzTest {
public static void main(String[] args) {
try {
// 使用 StdSchedulerFactory.getDefaultScheduler() 方法从标准工厂中获取默认的调度器实例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 使用 scheduler.start() 方法启动调度器,使其能够调度和执行任务
scheduler.start();
//使用 scheduler.shutdown() 方法关闭调度器,停止所有正在进行的任务调度
scheduler.shutdown();
} catch (SchedulerException se) {
se.printStackTrace();
}
}
}
-
输出:
到此,运行环境配置完成。 -
添加调度任务
任务类:
package com.example.quartz_task.job;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("hello world");
}
}
package com.guo.quartztest;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.JobBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
public class QuartzTest {
public static void main(String[] args) {
try {
// Grab the Scheduler instance from the Factory
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// and start it off
scheduler.start();
// 定义一个任务并将其与HelloJob类关联
JobDetail job = newJob(HelloJob.class) // 使用newJob方法创建一个JobDetail实例,指定任务类为HelloJob
.withIdentity("job1", "group1") // 设置任务的名称为"job1",组名为"group1"
.build(); // 构建JobDetail实例
// 定义一个触发器,立即运行任务,然后每40秒重复一次
Trigger trigger = newTrigger() // 使用newTrigger方法创建一个Trigger实例
.withIdentity("trigger1", "group1") // 设置触发器的名称为"trigger1",组名为"group1"
.startNow() // 设置触发器立即生效
.withSchedule(simpleSchedule() // 设置触发器的调度计划
.withIntervalInSeconds(40) // 设置任务执行的时间间隔为40秒
.repeatForever()) // 设置任务无限次重复执行
.build(); // 构建Trigger实例
// 告诉Quartz使用我们的触发器调度任务
scheduler.scheduleJob(job, trigger); // 使用scheduleJob方法将任务和触发器注册到调度器
scheduler.shutdown();
} catch (SchedulerException se) {
se.printStackTrace();
}
}
}
输出:
上面的任务调度流程是:定义任务和触发器;然后通过调度器关联任务和触发器。
我们也可以不通过调度器关联任务和触发器,在定义触发器的时候就指定触发的任务。
任务调度流程变成:
- 定义任务:
代码种添加.storeDurably()
。将Job 设置为“持久的”,不会因为没有 Trigger 而从调度器中移除。
在没有立即调度的情况下添加一个 Job,必须将Job标记为持久的 - 定义触发器并关联任务
代码中添加.forJob("job","group1")
- 使用调度器启动触发器。
代码添加:scheduler.addJob(job, false);
将一个 JobDetail对象添加到调度器中。如果不这样做,调度器不会记录该 Job,因此在你试图调度或执行这个 Job时,调度器将无法找到它,并可能抛出异常。
代码如下:
// Grab the Scheduler instance from the Factory
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// and start it off
scheduler.start();
// 定义一个任务并将其与HelloJob类关联
JobDetail job = newJob(HelloJob.class) // 使用newJob方法创建一个JobDetail实例,指定任务类为HelloJob
.storeDurably()//设置为持久的
.withIdentity("job", "group1") // 设置任务的名称为"job1",组名为"group1"
.build(); // 构建JobDetail实例
// 定义一个触发器,立即运行任务,然后每40秒重复一次
Trigger trigger = newTrigger() // 使用newTrigger方法创建一个Trigger实例
.withIdentity("trigger1", "group1") // 设置触发器的名称为"trigger1",组名为"group1"
.forJob("job","group1")
.startNow() // 设置触发器立即生效
.withSchedule(simpleSchedule() // 设置触发器的调度计划
.withIntervalInSeconds(40) // 设置任务执行的时间间隔为40秒
.repeatForever()) // 设置任务无限次重复执行
.build(); // 构建Trigger实例
//将job告诉调度器
scheduler.addJob(job,false);
// 告诉Quartz使用我们的触发器调度任务
scheduler.scheduleJob(trigger);
Thread.sleep(20);
scheduler.shutdown();
上面涉及到的任务、调度器、触发器的逻辑框架如下图:
- 创建任务调度器,去启动触发器
- 触发器通过相应的规则,去调度任务
- 任务的一些属性包装在JobDetail里
调度器、触发器、任务详情之间的关系:
- 调度器可以调度多个触发器
- 触发器只能调度一个任务
- 一个JobDetail可以被多个触发器调度
- 一个Job可以关联多个JobDetail
三、CronExpression
在定义触发器的时候,可以使用Cron表达式来替代函数式的定义触发规则:
// 定义一个触发器,立即运行任务,然后每40秒重复一次
Trigger trigger = newTrigger() // 使用newTrigger方法创建一个Trigger实例
.withIdentity("trigger1", "group1") // 设置触发器的名称为"trigger1",组名为"group1"
.startNow() // 设置触发器立即生效
.withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ? *")) // 设置任务无限次重复执行
.build(); // 构建Trigger实例
* * * * * ? *
解读:
符合解释:
四、传入变量,依赖注入
4.1 传入变量
场景:需要在任务调度的时候,给任务Job传递一些参数
- 使用usingJobData
- JobDetail的usingJobData
- 也可在trigger上使用usingJobData
// 定义一个任务并将其与HelloJob类关联
JobDetail job = newJob(HelloJob.class) // 使用newJob方法创建一个JobDetail实例,指定任务类为HelloJob
.withIdentity("job1", "group1") // 设置任务的名称为"job1",组名为"group1"
.usingJobData("key1","value1")//传递参数
.build(); // 构建JobDetail实例
// 定义一个触发器,立即运行任务,然后每40秒重复一次
Trigger trigger = newTrigger() // 使用newTrigger方法创建一个Trigger实例
.withIdentity("trigger1", "group1") // 设置触发器的名称为"trigger1",组名为"group1"
.usingJobData("key2","value2")
.startNow() // 设置触发器立即生效
.withSchedule(simpleSchedule() // 设置触发器的调度计划
.withIntervalInSeconds(40) // 设置任务执行的时间间隔为40秒
.repeatForever()) // 设置任务无限次重复执行
.build(); // 构建Trigger实例
- 任务类
package com.example.quartz_task.job;
import org.quartz.*;
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//通过上下文获取任务详情
JobDetail jobDetail = jobExecutionContext.getJobDetail();
//通过上下文获取触发器详情
Trigger trigger = jobExecutionContext.getTrigger();
//获取数据
System.out.println(jobDetail.getJobDataMap().get("key1"));
System.out.println(trigger.getJobDataMap().get("key2"));
}
}
输出:
此外也可以通过jobExecutionContext.getMergedJobDataMap().get("key");
来获取相应的键值对数据。
如果detailJob和trigger存在相同的key呢?
jobExecutionContext.getMergedJobDataMap()会获取哪一个的?
这里我们将两者的key都改成"key",最后输出的是“value2”,因此在detailJob和trigger都存在相同的键,会优先获取trigger的键值对值。
如果某个key是经常使用的,可以将其作为Job类的属性。而不用通过get方法去获取了。
4.2 依赖注入
以上的方式都是固定死的传值方式。在实际应用中,我们通常会结合spring以获取一个动态变化的值。
如,下面存在一个service类,在实际应用中,希望将这个service的返回结果作为参数传入。
package service;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
public String hello(){
return "say hello";
}
}
在任务类:
@Component
public class HelloJob implements Job {
@Autowired
private HelloService helloService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(helloService);
}
}
但是发现在启动Spring项目后,输出的却是null:
这是因为,触发器在触发任务的时候,quartz会对Job进行实例化;因为实例化是由quartz实例化的,因此Spring注解这些quartz是不能够识别的,因此不存在依赖注入。
我们可以通过Spring的上下文去获取的这个服务的输出:
- 先构建一个能够让外部获取上下文的类
package com.example.quartz_task.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringContextUtil implements ApplicationContextAware {
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
- 通过类获取Spring的bean对象
package com.example.quartz_task.job;
import com.example.quartz_task.util.SpringContextUtil;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.example.quartz_task.service.HelloService;
@Component
public class HelloJob implements Job {
@Autowired
private HelloService helloService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//通过Spring上下文获取Bean对象
helloService = SpringContextUtil.applicationContext.getBean(HelloService.class);
System.out.println(helloService.hello());
}
}
输出:
五、Quartz配置
quartz存在一个配置文件quartz.properties,如果自己不定义就会采取默认值。quartz.properties内容如下:
# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#
#指定调度器实例的名称,默认值为 DefaultQuartzScheduler。
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
#指定线程池中的线程数,默认值为 10。
org.quartz.threadPool.threadCount: 10
#指定线程池中线程的优先级,范围是 1(最低)到 10(最高)。这里的优先级设置为 5。
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
#指定 Quartz 处理触发器错失(misfire)时间的阈值,单位为毫秒。这里设置为 60000 毫秒(即 60 秒)。
org.quartz.jobStore.misfireThreshold: 60000
#指定 Quartz 的作业存储类型,这里使用 RAMJobStore,即将所有调度数据保存在内存中,而不是持久化到数据库。
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
配置参数详细信息可以看官网配置信息介绍
六、SpringBoot整合Quartz
根据官网,Spring Boot会自动为我们配置调度器,所以我们不需要通过工厂的方式去定义一个调度器了。
接下来只需要我们手动的定义JobDetail和Trigger的Bean,Spring Boot会自动获取并与Scheduler关联。
6.1 定义Job和trigger–第一种方式
class MySJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
}
}
QuartzJobBean是实现了Job的一个抽象类,里面对原来的execute进行了修改(添加了Spring的一些东西),然后再调用executeInternal。源码如下,因此我们从原来的方式改成:继承QuartzJobBean并重新executeInternal
public abstract class QuartzJobBean implements Job {
public QuartzJobBean() {
}
public final void execute(JobExecutionContext context) throws JobExecutionException {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValues(context.getScheduler().getContext());
pvs.addPropertyValues(context.getMergedJobDataMap());
bw.setPropertyValues(pvs, true);
} catch (SchedulerException var4) {
throw new JobExecutionException(var4);
}
this.executeInternal(context);
}
protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
}
详细步骤:
- 定义Job
package com.example.quartz_task.job;
import com.example.quartz_task.service.HelloService;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
public class MyJob extends QuartzJobBean {
@Autowired
private HelloService helloService;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println(helloService.hello());
}
}
- 定义JobDetail和Trigger
package com.example.quartz_task.config;
import com.example.quartz_task.job.MyJob;
import jakarta.annotation.PostConstruct;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class QuartzConfig {
@Autowired
private Scheduler scheduler;
@PostConstruct
public void initJob() throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(MyJob.class).build();
Trigger trigger = TriggerBuilder.newTrigger()
.startNow()
.build();
scheduler.scheduleJob(jobDetail,trigger);
}
}
- Spring的服务类
package com.example.quartz_task.service;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
public String hello(){
return "say hello";
}
}
- 启动服务,输出:
官网中,提到了:
6.1 定义Job和trigger–第二种方式
所以提供了第二种编写JobDtail和Trigger的方式:
package com.example.quartz_task.config;
import com.example.quartz_task.job.MyJob;
import jakarta.annotation.PostConstruct;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Configuration
public class QuartzConfig {
@Bean
public JobDetail SpringJobDetail(){
return JobBuilder.newJob(MyJob.class)
.withIdentity("springJobDetail")
.storeDurably()
.build();
}
@Bean
public Trigger springJobTrigger(){
return TriggerBuilder.newTrigger()
.forJob("springJobDetail")
.startNow()
.build();
}
}