SpringBoot定时任务 — Quartz最快速的集成及使用(定时任务未持久化)

一、前言

1.定时任务框架选型
1)简单的有Java自带的TimerScheduledExecutorService, Spring自带的Task
2)相较复杂的分布式定时任务中间件有XXL-JOBElasticJob等。
2.为什么是Quartz
简单的适用场景过于单一无法处理复杂的业务,而复杂的相较Quartz过于复杂且学习成本较高,仅用于新手学习而言Quartz较为合适。
3.文中涉及到的源码地址

二、引入

SpringBoot从2.0起整合了Quartz,因此只需要引入一个starter即可食用。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    <version>2.0.3.RELEASE</version>      <!-- 当前发布的最新版本 -->
</dependency>

三、配置

在application.yml中做如下配置即可。

spring:
  quartz:
    job-store-type: memory                             #所有任务相关内容存储在内存中
    scheduler-name: AnchorScheduler
    properties:
      org.quartz.scheduler.instanceId: 1122334         #集群中会用到,单节点无用,不填、填AUTO都可以
      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
      org.quartz.threadPool.threadCount: 9             #线程数
      org.quartz.threadPool.threadPriority: 5          #线程优先级
      org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
      org.quartz.jobStore.misfireThreshold: 60000      #作业最大延迟时间,毫秒
      org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore    #内存作业时负责跟踪调度所有工作数据

注:SpringBoot会根据yml中spring.quartz下的内容自动注入相关的Bean,具体实现代码在QuartzAutoConfiguration.class中。

四、基本

1)Quartz中有三个基本”组件”,由它们共同来定义、运行一个定时任务:
 — JobDetail,定时任务中的“任务”
 — Trigger,定时任务中的“定时”
 — Scheduler,定时任务的调度器(组装器);
2)还有一些常用的类:
 — JobKey,用于唯一标识一个JobDetail;
 — TriggerKey,用于唯一标识一个Trigger;

五、使用

1.创建任务

1)在Quartz中创建的所有定时任务都要实现Job接口,但是在SpringBoot中所有的定时任务只要继承QuartzJobBean类即可。
2)QuartzJobBean是一个抽象类,实现了Quartz的Job接口。
2)与Thread的run()方法类似,定时任务的具体实现写在executeInternal()方法中。
3)每创建一个新的定时任务,都需要新建一个Java类并继承QuartzJobBean、实现executeInternal()。

//SimpleJob是一个简单定时任务体,用于打印出当前线程名、当前时间及当前调用次数
public class SimpleJob extends QuartzJobBean {

    private final static AtomicInteger counter = new AtomicInteger(1);

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        LocalDateTime now = LocalDateTime.now();
        String name = Thread.currentThread().getName();
        System.out.println("Execute quartz \"SimpleJob\", threadName = \"" + name +
                "\", the " + counter.getAndIncrement() + "th execution, time = \"" +
                now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + "\"");
    }
}

2.创建JobDetail

使用建造者模式的JobBuilder来创建一个JobDetail对象。

JobDetail simpleJob = JobBuilder.newJob(SimpleJob.class)        //传入一个Job类
                                .withIdentity("SimpleJob", "AnchorJobs")    //(name, group)标识唯一一个JobDetail
                                .storeDurably()        //在没有Trigger关联的情况下保存该任务到调度器
                                .build();

注:
1)newJob()中传入的Job类必须是继承了QuartzJobBean的类。
2)withIdentity()中group可不传,不传时默认设为”DEFAULT”。
3)storeDurably()使JobDetail可在没有关联Trigger的情况下添加到调度器中,否则会抛异常。建议调用此方法。

3.创建Trigger

常用Trigger有两种:SimpleTriggerCronTrigger。二者最大的区别是CronTrigger支持Cron表达式,更灵活,因此使用CronTrigger居多。
1)创建SimpleTrigger

SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder
                                        .simpleSchedule()
                                        .withIntervalInSeconds(5)      //每5秒执行一次
                                        .repeatForever();              //无限循环执行
SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
                                      .withIdentity("SimpleTrigger", "AnchorTriggers")    //(name, group)唯一标识一个Trigger
                                      .startNow()
                                      .withSchedule(scheduleBuilder)
                                      .build();

2)创建CronTrigger

CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");    //Cron表达式,每5秒执行一次
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                                    .withIdentity("CronJob", "AnchorTriggers")    //(name, group)唯一标识一个Trigger
                                    .startNow()                         //调用scheduler.scheduleJob()后立即开始执行定时任务
                                    .withSchedule(scheduleBuilder)      //不同的scheduleBuilder
                                    .build();

4.启动任务

@Resource
private Scheduler scheduler;          //SpringBoot会根据我们上文的配置自动装载Scheduler,直接注入即可使用


public void start() {
    //或scheduler.scheduleJob(simpleJob, cronTrigger);
    scheduler.scheduleJob(simpleJob, simpleTrigger);
}

注:
1)一个Job只能绑定一个Trigger。同一个Job不能用两个Trigger去触发,同一个Trigger也不能去触发两个Job。
2)创建并启动一个定时任务的正常流程是:创建任务类 ——> 创建JobDetail ——> 创建Trigger ——> scheduler启动任务

六、增删查改

增删查改都是基于Scheduler提供方法的二次封装。

1.新增

由于SimpleTrigger的局限性,这里只封装了新增使用CronTrigger的任务的方法。

/**
 *  className是任务类的类名,cronExpression是Cron表达式
 */
public Date addAndStartCronJob(String className, String cronExpression) throws Exception {
    Class clazz = null;
    try {
        //用于校验类是否存在并实例化该的任务类
        String entireClassName = JOB_PACKAGE + "." + className;
        clazz = Class.forName(entireClassName);
    } catch (ClassNotFoundException e) {
        throw new Exception("Class \"" + className + "\" doesn't exist.");
    }
    //将类名作为Quartz的任务名,“类名 + Trigger”作为Trigger名
    JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(className, JOB_GROUP).storeDurably().build();
    CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(className + "Trigger", TRIGGER_GROUP)
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                .build();
    return scheduler.scheduleJob(jobDetail, trigger);        //新增并启动
    }

注:JOB_PACKAGE、JOB_GROUP、TRIGGER_GROUP是三个String常量,JOB_PACKAGE是任务类所在包的包名。

2.删除

这里删除分为两类:暂停删除
1)暂停

public void pauseJob(String className) throws Exception {
    JobKey jobKey = new JobKey(className, JOB_GROUP);
    if (scheduler.checkExists(jobKey)) {
        scheduler.pauseJob(jobKey);        //存在对应的恢复方法scheduler.resumeJob(jobKey);
    } else {
        throw new Exception("Job \"" + className + "\" doesn't exist.");
    }
}
public void pauseAll() throws SchedulerException {
    scheduler.pauseAll();        //存在对应的恢复方法scheduler.resumeAll(jobKey);
}

2)删除

public boolean deleteJob(String className) throws Exception {
    JobKey jobKey = new JobKey(className, JOB_GROUP);
    if (scheduler.checkExists(jobKey)) {
        return scheduler.deleteJob(jobKey);        //
    } else {
        throw new Exception("Job \"" + className + "\" doesn't exist.");
    }
}

注:暂停和删除scheduler都有批量操作的接口,这里未做封装。需要说明的是暂停的批量操作是针对group,删除的批量操作是针对(name,group)。

3.查询

封装了两个查询接口:根据状态查询定时任务列表、查询某个定时任务的状态。

public List<String> getJobsByState(String queryState) throws Exception {
    if (!TRIGGER_STATUS.containsKey(queryState)) {
        throw new Exception("queryState doesn't exist.");
    }
    List<String> jobNames = new ArrayList<>();
    List<String> jobGroupNames = scheduler.getJobGroupNames();         //获取所有注册到调度器任务的group
    for (String groupName : jobGroupNames) {
        for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {        //获取每个group下的所有任务
            String triggerName = jobKey.getName() + "Trigger";
            Trigger.TriggerState state = scheduler.getTriggerState(new TriggerKey(triggerName, TRIGGER_GROUP));    //获取当前任务的执行状态
            //比对状态
            if (state.equals(TRIGGER_STATUS.get(queryState))) {         //TRIGGER_STATUS是个Map,封装了TriggerState
                jobNames.add(jobKey.getName());
            }
        }
    }
    return jobNames;
}
public String getJobStatus(String className) throws Exception {
    JobKey jobKey = new JobKey(className, JOB_GROUP);
    if (scheduler.checkExists(jobKey)) {
        String triggerName = className + "Trigger";
        TriggerKey triggerKey = new TriggerKey(triggerName, TRIGGER_GROUP);
        Trigger.TriggerState state = scheduler.getTriggerState(triggerKey);
        return JOB_STATUS.get(state);         //JOB_STATUS是个Map,封装了TriggerState
    } else {
        throw new Exception("Job \"" + className + "\" doesn't exist.");
    }
}

注:Scheduler有个getCurrentlyExecutingJobs()方法,但是此方法获取的是请求瞬间正在执行的任务。

4.修改

用于修改某个任务的Cron。

public Date modifyJobCron(String className, String cronExpression) throws Exception {
    try {
        //用于校验改任务的实现类是否存在
           String entireClassName = JOB_PACKAGE + "." + className;
           Class clazz = Class.forName(entireClassName);
    } catch (ClassNotFoundException e) {
        throw new Exception("Class \"" + className + "\" doesn't exist.");
    }
    String triggerName = className + "Trigger";
    TriggerKey triggerKey = new TriggerKey(triggerName, TRIGGER_GROUP);
    CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(triggerKey)
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                .build();
    return scheduler.rescheduleJob(triggerKey, trigger);           //更新并立即执行任务
}