Qquartz定时任务
- 一、任务调度概念
- 1.1 什么是任务调度
- 1.2 为什么要使用分布式调度
- 二、Quartz快速上手
- 三、CronExpression
- 四、传入变量,依赖注入
- 4.1 传入变量
- 4.2 依赖注入
- 五、Quartz配置
- 六、SpringBoot整合Quartz
- 6.1 定义Job和trigger--第一种方式
- 6.1 定义Job和trigger--第二种方式
- 七、Quartz持久化
一、任务调度概念
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 {@Overridepublic 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 FactoryScheduler scheduler = StdSchedulerFactory.getDefaultScheduler();// and start it offscheduler.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 FactoryScheduler scheduler = StdSchedulerFactory.getDefaultScheduler();// and start it offscheduler.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 {@Overridepublic 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 {@Autowiredprivate HelloService helloService;@Overridepublic 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;@Overridepublic 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 {@Autowiredprivate HelloService helloService;@Overridepublic 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: DefaultQuartzSchedulerorg.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: falseorg.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 {@Overrideprotected 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 {@Autowiredprivate HelloService helloService;@Overrideprotected 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 {@Autowiredprivate Scheduler scheduler;@PostConstructpublic 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 {@Beanpublic JobDetail SpringJobDetail(){return JobBuilder.newJob(MyJob.class).withIdentity("springJobDetail").storeDurably().build();}@Beanpublic Trigger springJobTrigger(){return TriggerBuilder.newTrigger().forJob("springJobDetail").startNow().build();}
}