引言
针对耗时操作、定时操作和周期操作等,常常会通过异步线程调用执行这些操作。实现定时异步操作有多种方案,比如使用Spring boot异步机制和调度框架等。Spring boot异步机制使用简单,只需定义异步方法便可执行异步操作,但是任务监控、任务复杂操作不易实现。调度框架管理任务更加灵活,提供更加丰富的功能,常见的调度框架有Quartz、ElasticJob和XXL-JOB等
一、Quartz简介
Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵 活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,EJB 作业预构 建,JavaMail 及其它,支持 cron-like 表达式等等。
Quartz 具有如下几个关键概念:
- Job 定义了一个任务的执行逻辑。
- JobDetail 是 Job 的一个执行实例。
- Trigger 是 JobDetail 的执行计划,定义了 JobDetail 什么时候执行、异常情况怎么执行等。
- Scheduler是调度容器,用来注册 JobDetail 和 Trigger,管理执行任务的执行、暂停、恢复等操作。
- 监听器,分为 JobListener、TriggerListener和SchedulerListener,分别用来监听 JobDetail、Trigger 和Scheduler 的事件并做出增强处理,给任务协调执行提供了支持。
1、Job
通过实现Job接口,定义任务的执行逻辑。在Job接口中,有一个方法 execute(JobExecutionContext var1),JobExecutionContext为任务执行的上下文,可以通过这个参数来获取执行任务实例JobDetail、任务参数JobDataMap、执行计划Trigger和Scheduler等,JobExecutionContext参数为实现任务执行逻辑提供了较高的灵活性 。
java
public interface Job {
void execute(JobExecutionContext var1) throws JobExecutionException;
}
2、JobDetail
JobDetail 表示一个 Job 的执行实例,通过 JobBuilder 可以创建一个JobDetail实例,每个JobDetail需要指定一个 Identity 标识,Identity 标识分为 name 和 group 两部分,需要保证调度上下文中 Identity 唯一。
java
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(name, group)
// .storeDurably() 用来持久化,即执行该任务后,数据库不会删除该任务,下次定义Trigger时还可以使用
.setJobData(jobDataMap)
.build();
3、Trigger
使用Trigger可以配置JobDetail的执行计划,通过使用 TriggerBuilder 可以创建一个 Trigger 实例,并通过 withIdentity 方法可以指定 JobDetail 实例,通过 withSchedule 方法配置调度策略。
java
TriggerBuilder.newTrigger()
.withIdentity(jobKey.getName(), jobKey.getGroup())
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(seconds).withRepeatCount(1))
.build();
ScheduleBuilder用来配置调度策略,ScheduleBuilder具有如下四个子类
4、Scheduler
Scheduler 用来注册JobDetail、Trigger和监听器,可以管理任务的执行生命周期。
java
/**
该接口具有较多方法,只列举几个较重要的。通过该接口还可以获取调度容器有哪些JobDetail和Trigger
*/
public interface Scheduler {
// 添加任务
Date scheduleJob(JobDetail var1, Trigger var2) throws SchedulerException;
// 添加任务,对应的JobDetail已持久化
Date scheduleJob(Trigger var1) throws SchedulerException;
// 修改执行计划
Date rescheduleJob(TriggerKey var1, Trigger var2) throws SchedulerException;
// 删除任务
boolean deleteJob(JobKey var1) throws SchedulerException;
// 暂停任务
void pauseJob(JobKey var1) throws SchedulerException;
// 恢复任务
void resumeJob(JobKey var1) throws SchedulerException;
// 获取监听器,获取的ListenerManager可以注册监听器
ListenerManager getListenerManager() throws SchedulerException;
}
Scheduler 实现类如下图所示,默认使用 StdScheduler
5、监听器
监听器分为 JobListener、TriggerListener 和 SchedulerListener, JobListener 用来监听JobDetail, TriggerListener 用来监听Trigger, SchedulerListener 用来监听 Scheduler。通过使用 Scheduler.getListenerManager 方法获取的 ListenerManager 实例对象可以注册监听器。
java
public interface JobListener {
String getName();
// 任务执行前处理
void jobToBeExecuted(JobExecutionContext var1);
// 在JobDetail即将被执行,但又被TriggerListener否决时会调用该方法
void jobExecutionVetoed(JobExecutionContext var1);
// 任务执行后处理
void jobWasExecuted(JobExecutionContext var1, JobExecutionException var2);
}
java
public interface TriggerListener {
String getName();
// 调度器触发时处理
void triggerFired(Trigger var1, JobExecutionContext var2);
// 在Trigger即将被执行,但又被Scheduler否决时会调用该方法
boolean vetoJobExecution(Trigger var1, JobExecutionContext var2);
// 触发器错失触发时处理
void triggerMisfired(Trigger var1);
// 触发器触发并且对应的JobDetail执行完后处理
void triggerComplete(Trigger var1, JobExecutionContext var2, Trigger.CompletedExecutionInstruction var3);
}
java
public interface SchedulerListener {
void jobScheduled(Trigger var1);
void jobUnscheduled(TriggerKey var1);
void triggerFinalized(Trigger var1);
void triggerPaused(TriggerKey var1);
void triggersPaused(String var1);
void triggerResumed(TriggerKey var1);
void triggersResumed(String var1);
void jobAdded(JobDetail var1);
void jobDeleted(JobKey var1);
void jobPaused(JobKey var1);
void jobsPaused(String var1);
void jobResumed(JobKey var1);
void jobsResumed(String var1);
void schedulerError(String var1, SchedulerException var2);
void schedulerInStandbyMode();
void schedulerStarted();
void schedulerStarting();
void schedulerShutdown();
void schedulerShuttingdown();
void schedulingDataCleared();
}
二、Quartz 简单使用
Spring boot 使用 Quartz 有两种任务存储方式,分别是内存和数据库,这里介绍基于数据库存储方式的使用
1、引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
在该依赖中,QuartzAutoConfiguration 提供了 Quartz 的自动配置,该类创建了一个Scheduler Bean,具体类为Stdcheduler,同时会自动扫描 JobDetail 和 Trigger Bean,并将这些 Bean 存放到数据库中。
2、添加 Quartz 配置
在 application.yml 文件中添加配置
yml
spring:
application:
name: quartz-task
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/quartz_test?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
username:
password:
quartz:
job-store-type: jdbc
jdbc:
initialize-schema: always # 启动时初始化 Quartz 数据表方式
Quartz 数据库表文件可以查看 org.quartz-scheduler 包,路径为:quartz-x.x.x.jar/org/quartz/impl/jdbcjobstore ,在 SQL脚本中首先会删除表,再创建表。如果需要在项目启动时保留表中数据,可以让项目自己管理 quartz 的 SQL 脚本,不执行 Quartz 的数据库初始化。SQL文件如下图所示
3、定义任务 Job
java
@Slf4j
public class HelloWorldJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDetail jobDetail = jobExecutionContext.getJobDetail();
JobKey key = jobDetail.getKey();
log.info("HelloWorldJob begin. name: {}, group: {}", key.getName(), key.getGroup());
Trigger trigger = jobExecutionContext.getTrigger();
JobDataMap jobDataMap = trigger.getJobDataMap();
Set<Map.Entry<String, Object>> entrySet = jobDataMap.entrySet();
for (Map.Entry<String, Object> entry : entrySet) {
log.info("trigger data: key = {}, value = {}.", entry.getKey(), entry.getValue());
}
log.info("HelloWorldJob end. name: {}, group: {}", key.getName(), key.getGroup());
}
}
4、配置任务执行实例
执行任务有两种方式,一种是定义JobDetail和Trigger Bean,并将其注入到Spring容器中;另一种时,调用 Scheduler Bean提供的方法添加任务。
1、方式一
定义JobDetail和Trigger Bean
java
@Configuration
public class QuartzConfig {
@Bean
public JobDetail helloWorldJobDetail() {
JobKey jobKey = new JobKey("HelloWorld", "DefaultGroup");
return JobBuilder.newJob(HelloWorldJob.class)
.withIdentity(jobKey)
.storeDurably()
.build();
}
@Bean
public Trigger helloWorldTrigger() {
JobKey jobKey = new JobKey("HelloWorld", "DefaultGroup");
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
Date now = new Date();
String nowAsString = sdf.format(now);
return TriggerBuilder.newTrigger()
.forJob(jobKey)
.usingJobData("execute time", nowAsString)
.startNow()
// 隔3秒执行一次,总共执行 3+1 次
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).withRepeatCount(3))
.build();
}
}
执行结果:
2、方式二
调用 Scheduler Bean 方法添加任务、暂停任务等
java
@Component
public class SchedulerManager {
@Autowired
private Scheduler scheduler;
}
自动注入Scheduler后,可调用它的方法
java
// 部分方法,添加任务、暂停任务、恢复任务
public interface Scheduler {
List<JobExecutionContext> getCurrentlyExecutingJobs() throws ShedulerException;
ListenerManager getListenerManager() throws SchedulerException;
Date scheduleJob(JobDetail var1, Trigger var2) throws SchedulerException;
Date scheduleJob(Trigger var1) throws SchedulerException;
void scheduleJobs(Map<JobDetail, Set<? extends Trigger>> var1, boolean var2) throws SchedulerException;
void scheduleJob(JobDetail var1, Set<? extends Trigger> var2, boolean var3) throws SchedulerException;
boolean unscheduleJob(TriggerKey var1) throws SchedulerException;
boolean unscheduleJobs(List<TriggerKey> var1) throws SchedulerException;
// 修改执行计划
Date rescheduleJob(TriggerKey var1, Trigger var2) throws SchedulerException;
void addJob(JobDetail var1, boolean var2) throws SchedulerException;
void addJob(JobDetail var1, boolean var2, boolean var3) throws SchedulerException;
boolean deleteJob(JobKey var1) throws SchedulerException;
boolean deleteJobs(List<JobKey> var1) throws SchedulerException;
void triggerJob(JobKey var1) throws SchedulerException;
void triggerJob(JobKey var1, JobDataMap var2) throws SchedulerException;
void pauseJob(JobKey var1) throws SchedulerException;
void pauseJobs(GroupMatcher<JobKey> var1) throws SchedulerException;
void pauseTrigger(TriggerKey var1) throws SchedulerException;
void pauseTriggers(GroupMatcher<TriggerKey> var1) throws SchedulerException;
void resumeJob(JobKey var1) throws SchedulerException;
void resumeJobs(GroupMatcher<JobKey> var1) throws SchedulerException;
void resumeTrigger(TriggerKey var1) throws SchedulerException;
void resumeTriggers(GroupMatcher<TriggerKey> var1) throws SchedulerException;
void pauseAll() throws SchedulerException;
void resumeAll() throws SchedulerException;
List<String> getJobGroupNames() throws SchedulerException;
}
5、@DisallowConcurrentExecution和@PersistJobDataAfterExecution注释说明
- @DisallowConcurrentExecution:用在 Job 接口实现类上,用来保证不会同时执行该 Job 接口实现类执行实例
- @PersistJobDataAfterExecution:用在 Job 接口实现类,表示在正常执行完Job后, JobDataMap中的数据应该被改动,下次执行相同的job时,会接收到已经更新的数据
6、如何在 Job 执行逻辑中使用 Spring 容器 Bean
由于 Job 接口实现类不被 Spring 容器管理,无法直接通过自动注入注入Bean,需要定义一个静态方法获取 Spring Bean。
java
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtil.applicationContext = applicationContext;
}
/**
* 根据名称获取bean
*
* @param beanName
* @return
*/
public static Object getBean(String beanName) {
if (beanName == null) {
return null;
}
return applicationContext.getBean(beanName);
}
public static <T> T getBean(Class<T> clazz) {
if (clazz == null) {
return null;
}
return applicationContext.getBean(clazz);
}
}
三、总结
通过使用 Quartz 调度框架,可以灵活的管理任务。在 Quartz 中,具有如下几个关键概念:Job、JobDetail、Trigger、Scheduler 和 Listener。另外,管理、监控任务等操作,关键在于使用 Scheduler 方法。