【Quartz】初识与基本使用

个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


前言

本文只做基于SpringBoot的示例,其余版本的请自行查阅资料,大同小异


1.介绍

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能:

  • 持久性作业 - 就是保持调度定时的状态;
  • 作业管理 - 对调度作业进行有效的管理;

使用quartz的调度器统一管理定时任务,可以动态的添加、删除、暂停等

推荐几个博客,写的真的挺好

2.使用步骤

2.1.4静态定时器

静态定时器即主要使用代码控制,一旦项目部署将无法对定时器做出调整(增删改),适用于强制执行而不需要灵活变通的任务

  1. 添加quartz依赖

    <!-- 定时器依赖 -->
    <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.3.2</version>
    </dependency>
    
  2. 创建需要执行的任务类MyTask,必须继承并实现QuartzJobBean

    public class MyTask extends QuartzJobBean {
        /**
         * 计数器
         */
        private static Integer count = 0;
    
        /**
         * 执行内容
         *
         * @param jobExecutionContext 上下文
         */
        @Override
        protected void executeInternal(JobExecutionContext jobExecutionContext) {
            JobDetail jobDetail = jobExecutionContext.getJobDetail();
            Trigger trigger = jobExecutionContext.getTrigger();
            synchronized (count) {
                System.out.println(""
                        + new Date()    //打印时间
                        + " :" + count  //打印计时器
                        + "  jobDetailKey : " + jobDetail.getKey().toString()   //打印jobDetail的key
                        + "  jobDetailData : " + jobDetail.getJobDataMap().getString("jobName") //打印来自jobDetail的参数
                        + "  triggerKey : " + trigger.getKey().toString()   //打印trigger的key
                        + "  triggerData : " + trigger.getJobDataMap().getString("triggerName") //打印来自trigger的参数
                );
                count++;
            }
        }
    }
    
  3. 创建quartz配置文件QuartzConfig.java

    1. 定义业务组件myJob1(),配置需要执行的任务类、业务自定义身份、转递给任务的参数等等
    2. 定义触发器myTrigger1(),配置关联的业务组件、触发器自定义身份、转递给任务的参数等等
    3. 别忘了使用@Configuration标记配置文件类、@Bean标记业务组件和触发器
    @Configuration
    public class QuartzConfig {
        @Bean
        public JobDetail myJob1() {
            JobDetail jobDetail = JobBuilder.newJob(MyTask.class)
                    .withIdentity("myJob1", "myJobGroup1")
                    //JobDataMap可以给任务execute传递参数,且可传递多个
                    .usingJobData("jobName", "myJob1")
                    .storeDurably()
                    .build();
            return jobDetail;
        }
    
        @Bean
        public Trigger myTrigger1() {
            Trigger trigger = TriggerBuilder.newTrigger()
                    .forJob(myJob1())
                    .withIdentity("myTrigger1", "myTriggerGroup1")
                    //JobDataMap可以给任务execute传递参数,且可传递多个
                    .usingJobData("triggerName", "myTrigger1")
                    .startNow()
                    //.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
                    .withSchedule(CronScheduleBuilder.cronSchedule("*/3 * * * * ?"))
                    .build();
            return trigger;
        }
    
        @Bean
        public Trigger myTrigger2() {
            Trigger trigger = TriggerBuilder.newTrigger()
                    .forJob(myJob1())
                    .withIdentity("myTrigger2", "myTriggerGroup1")
                    .usingJobData("triggerName", "myTrigger2")
                    .startNow()
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
    //                .withSchedule(CronScheduleBuilder.cronSchedule("*/3 * * * * ?"))
                    .build();
            return trigger;
        }
    }
    

执行结果如下

【Quartz】初识与基本使用

  • myTrigger1和myTrigger2均使用业务组件myJob1
  • myTrigger1每隔3秒触发一次
  • myTrigger2每隔5秒触发一次

2.2.动态定时器

动态定时器即可以对定时器动态进行调整,通常由前端页面和数据库配合,从而灵活调整定时器

因为需要动态管理,那么数据必须保证持久化,否则重新启动项目就什么都不剩了,那肯定不得行

添加quartz依赖

        <!-- 定时器依赖 -->
        <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>

持久化配置文件

写入配置文件即可,仅编写quartz相关部分,其余按照自己需求即可,不影响

推荐下面两篇博文,介绍的很详细,此处只做简单实现

spring:
  quartz:
    #相关属性配置
    properties:
      org:
        quartz:
          scheduler:
            instanceName: quartzScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: false
            clusterCheckinInterval: 10000
            useProperties: false
            misfireThreshold : 5000
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true
    #数据库方式
    job-store-type: JDBC
    #初始化表结构
    jdbc:
      initialize-schema: NEVER

建表

表需要依照quartz官方给出的实例,可以到http://www.quartz-scheduler.org/downloads/下载后解压,将目录\src\org\quartz\impl\jdbcjobstore\tables_mysql_innodb.sql里的语句在数据库直接执行即可

嫌卡或者懒得可以在这个地址获取2.3.0版本https://gitee.com/echo_ye/assets/blob/master/doc/tables_mysql_innodb.sql

数据封装类和查询方法

数据封装类需要包括前后端传递的所有参数,查询则需要向quartz相关的表中查询

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yezi_tool.demo_basic.mapper.JobMapper">
    <select id="listJob" resultType="com.yezi_tool.demo_basic.entity.QuartzJob">
        SELECT
          job.JOB_NAME as jobName,
          job.JOB_GROUP as jobGroup,
          job.DESCRIPTION as description,
          job.JOB_CLASS_NAME as jobClassName,
          cron.CRON_EXPRESSION as cronExpression,
          tri.TRIGGER_NAME as triggerName,
          tri.TRIGGER_STATE as triggerState,
          job.JOB_NAME as oldJobName,
          job.JOB_GROUP as oldJobGroup
        FROM qrtz_job_details AS job
        LEFT JOIN qrtz_triggers AS tri ON job.JOB_NAME = tri.JOB_NAME
        LEFT JOIN qrtz_cron_triggers AS cron ON cron.TRIGGER_NAME = tri.TRIGGER_NAME
        WHERE tri.TRIGGER_TYPE = 'CRON'
    </select>
</mapper>
package com.yezi_tool.demo_basic.mapper;

import com.yezi_tool.demo_basic.entity.QuartzJob;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface JobMapper {
    List<QuartzJob> listJob();
}

package com.yezi_tool.demo_basic.entity;

import lombok.Data;

import java.util.List;
import java.util.Map;

@Data
public class QuartzJob {
    private String jobName;//任务名称
    private String jobGroup;//任务分组
    private String description;//任务描述
    private String jobClassName;//执行类
    private String cronExpression;//执行时间

    private String triggerState;//任务状态

    private List<Map<String,Object>> dataMap;//参数
    
    private List<Map<String, Object>> jobDataParam;//备用数据域


    public QuartzJob() {
        super();
    }

    public QuartzJob(String jobName, String jobGroup, String description, String jobClassName, String cronExpression) {
        super();
        this.jobName = jobName;
        this.jobGroup = jobGroup;
        this.description = description;
        this.jobClassName = jobClassName;
        this.cronExpression = cronExpression;
    }
}

执行的任务类

与静态的任务类相同,可以建立多个,但是最好保证在同一个目录下,方便实例化的时候确认路径

本案例的任务类放置在目录com.yezi_tool.demo_basic.test

服务层方法

控制层为主要代码,在这里对定时任务进行增删改查等操作

因为懒就不写接口实现了。。。实际使用中请自己补上

重点为创建定时任务的方法。步骤如下

  1. 检查任务是否已存在,若是,则删除任务
  2. 根据任务类的名字(路径固定),实例化任务类task
  3. 创建定时任务job,设置目标任务类、job名、分组名、传递给任务的数据、任务描述等参数
  4. 创建触发器trigger,设置触发器的名字、分组、触发时间、传递给任务的数据等参数
  5. 通过调度程序schedule将job和trigger进行绑定并启动

也可在trigger中绑定job,那么schedule直接启动trigger即可

@Service
public class QuartzService {

    public String QUARTZ_TASK_PATH_HEAD = "com.yezi_tool.demo_basic.test" + ".";

    private final Scheduler scheduler;

    private final JobMapper jobMapper;

    public QuartzService(Scheduler scheduler, JobMapper jobMapper) {
        this.scheduler = scheduler;
        this.jobMapper = jobMapper;
    }

    public List<QuartzJob> list(String jobName) {
        List<QuartzJob> list = jobMapper.listJob();
        list.forEach(m -> {
            //只查询类名,无视路径
            m.setJobClassName(m.getJobClassName().replace(QUARTZ_TASK_PATH_HEAD, ""));
        });
        return list;
    }

    /**
     * 保存定时任务
     *
     * @param quartzJob 定时任务类
     * @throws Exception 抛出异常
     */
    public void save(QuartzJob quartzJob) throws Exception {
        try {
            //组装参数
            JobKey jobKey = new JobKey(quartzJob.getJobName(), quartzJob.getJobGroup());
            JobDataMap jobDataMap = new JobDataMap();
            if (quartzJob.getDataMap() != null) {
                for (Map<String, Object> map : quartzJob.getDataMap())
                    jobDataMap.putAll(map);
            }

            //删除旧的job
            if (scheduler.checkExists(jobKey)) {
                scheduler.deleteJob(jobKey);
            }

            //构建新的job
            Class clazz = Class.forName(QUARTZ_TASK_PATH_HEAD + quartzJob.getJobClassName());   //实例化目标任务类
            clazz.getDeclaredConstructor().newInstance();
            JobDetail jobDetail = JobBuilder
                    .newJob(clazz)//设置目标任务类
                    .withIdentity(quartzJob.getJobName(), quartzJob.getJobGroup())//设置job名和分组名
                    .usingJobData("jobName", quartzJob.getJobName())//传递的参数,可以自定义,可传递多组,以map形式传递
//                    .usingJobData(new JobDataMap(quartzJob.getDataMap()))
                    .withDescription(quartzJob.getDescription())//设置描述,可为空
                    .build();

            //构建新的触发器
            Trigger trigger = TriggerBuilder
                    .newTrigger()
//                    .forJob(jobDetail)//目标job,这里不设置,如果设置了将会自动触发
                    .withIdentity(quartzJob.getJobName() + "Trigger", quartzJob.getJobGroup())//设置job名和分组名
                    .usingJobData("triggerName", quartzJob.getJobName() + "Trigger")//传递的参数,可以自定义,可传递多组,以map形式传递
                    .usingJobData(jobDataMap)
                    .withSchedule(CronScheduleBuilder
                            .cronSchedule(quartzJob.getCronExpression().trim())
                            .withMisfireHandlingInstructionDoNothing()
                    )
                    .startNow()//现在就启动
//                    .startAt(new Date())//启动时间,可以自定义,与startNow冲突
                    .build();

            //通过schedule触发
            scheduler.scheduleJob(jobDetail, trigger);

        } catch (SchedulerException e) {
            throw new BaseException("保存定时任务失败");
        } catch (ClassNotFoundException e) {
            throw new BaseException("任务目标类不存在");
        }
    }


    /**
     * 启动任务
     *
     * @param jobName  任务名
     * @param jobGroup 任务分组
     * @param dataMap  数据,可为空
     * @throws Exception 相关异常
     */
    public void trigger(String jobName, String jobGroup, List<Map<String, Object>> dataMap) throws Exception {
        //组装参数
        JobKey jobKey = new JobKey(jobName, jobGroup);
        JobDataMap jobDataMap = new JobDataMap();
        if (dataMap != null) {
            for (Map<String, Object> map : dataMap)
                jobDataMap.putAll(map);
        }
        //启动触发器
        try {
            scheduler.triggerJob(jobKey, jobDataMap);
        } catch (SchedulerException e) {
            throw new BaseException("启动定时任务失败");
        }
    }

    /**
     * 暂停任务
     *
     * @param jobName  任务名
     * @param jobGroup 任务分组
     * @throws Exception 相关异常
     */
    public void pause(String jobName, String jobGroup) throws Exception {
        //组装参数
//        JobKey jobKey = new JobKey(jobName, jobGroup);
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", jobGroup);

        //暂停触发器
        try {
//            scheduler.pauseJob(jobKey);
            scheduler.pauseTrigger(triggerKey);
        } catch (SchedulerException e) {
            throw new BaseException("暂停定时任务失败");
        }
    }

    /**
     * 继续任务
     *
     * @param jobName  任务名
     * @param jobGroup 任务分组
     * @throws Exception 相关异常
     */
    public void resume(String jobName, String jobGroup) throws Exception {
        //组装参数
//        JobKey jobKey = new JobKey(jobName, jobGroup);
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", jobGroup);

        //继续触发器
        try {
//            scheduler.resumeJob(jobKey);
            scheduler.resumeTrigger(triggerKey);
        } catch (SchedulerException e) {
            throw new BaseException("启动定时任务失败");
        }
    }

    /**
     * 取消任务
     *
     * @param jobName  任务名
     * @param jobGroup 任务分组
     * @throws Exception 相关异常
     */
    public void cancel(String jobName, String jobGroup) throws Exception {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", jobGroup);
            JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
            //暂停
            scheduler.pauseTrigger(triggerKey);
            //解除绑定
            scheduler.unscheduleJob(triggerKey);
            //删除触发器
            scheduler.deleteJob(jobKey);
        } catch (SchedulerException e) {
            throw new BaseException("取消定时任务失败");
        }
    }

    /**
     * 取消全部任务
     *
     * @throws Exception 相关异常
     */
    public void cancelAll() throws Exception {
        List<QuartzJob> jobList = jobMapper.listJob();
        for (QuartzJob quartzJob : jobList) {
            cancel(quartzJob.getJobName(), quartzJob.getJobGroup());
        }
    }

    /**
     * 暂停全部任务
     *
     * @throws Exception 相关异常
     */
    public void pauseAll() throws Exception {
        try {
            scheduler.pauseAll();
        } catch (SchedulerException e) {
            throw new BaseException("暂停定时任务失败");
        }
    }

    /**
     * 恢复所有任务
     *
     * @throws Exception
     */
    public void resumeAll() throws Exception {
        try {
            scheduler.resumeAll();
        } catch (SchedulerException e) {
            throw new BaseException("启动定时任务失败");
        }
    }

}

控制层

直接调用服务层方法就可以了,没啥技术含量

这里只调用部分功能用于测试,请按照实际需求调整

@Controller
@RequestMapping("/quartz")
@Slf4j
public class QuartzController {
    private QuartzService quartzService;

    public QuartzController(QuartzService quartzService) {
        this.quartzService = quartzService;
    }

    @GetMapping("/list")
    @ResponseBody
    public ReturnMsg list(String jobName) {
        log.info("查询列表");
        return ReturnMsg.success(quartzService.list(jobName));
    }

    @PostMapping("/save")
    @ResponseBody
    public ReturnMsg save(@RequestBody QuartzJob quartzJob) throws Exception {
        log.info("保存");
        quartzService.save(quartzJob);
        return ReturnMsg.success();
    }

    @PostMapping("/pauseAll")
    @ResponseBody
    public ReturnMsg pauseAll() throws Exception {
        log.info("全部暂停");
        quartzService.pauseAll();
        return ReturnMsg.success();
    }

    @PostMapping("/resumeAll")
    @ResponseBody
    public ReturnMsg resumeAll() throws Exception {
        log.info("恢复全部");
        quartzService.resumeAll();
        return ReturnMsg.success();
    }

    @PostMapping("/cancelAll")
    @ResponseBody
    public ReturnMsg cancelAll() throws Exception {
        log.info("删除全部");
        quartzService.cancelAll();
        return ReturnMsg.success();
    }
}


测试结果

save的参数如下,其余请求为空参数

{
"jobName": "job1",
"jobGroup": "group1",
"description": "demoJob",
"jobClassName": "MyTask",
"cronExpression": "*/3 * * * * ?"
}

3.补充

  • job与trigger的关系

    job表示任务,trigger表示触发器

    实际上两者是1-N的关系,一个任务可以被多个trigger持有,而trigger只能绑定一个job,但为了方便管理建议按1-1处理

    因此对于job的操作(暂停、恢复)会同步至与之关联的trigger,部分操作如下

    • 暂停/恢复job时,暂停/恢复与之相关的所有trigger
    • 删除job时,解除所有trigger与自身的关联,解除失败时报错

    而对于trigger的操作并不会同步到job

  • 执行的时间

    先上图

    【Quartz】初识与基本使用

    可以看到trigger中自己生成了一个字段nextFireTime,即下一次执行时间

    到了nextFireTime时间则会执行一次任务,并将该字段更新

  • 执行失效的处理方案

    配置文件中有一条属性misfireThreshold = 5000,即失效阈值为5秒钟,表示一个任务若5秒内没有执行完成,则表示执行失效

    在对Trigger.withSchedule的时候对ScheduleBuilder设置处理方案

    • CronScheduleBuilder有4种方案,设置方法如下

      • 不设置:默认,智能处理。具体多智能我也不知道,但是这种不确定性显然是比较危险的,慎用
      • withMisfireHandlingInstructionIgnoreMisfires():忽略失效策略。服务恢复(包括项目启动)后,将一次性执行多次直至错过的任务全部补充。适用于部分场景。
      • withMisfireHandlingInstructionDoNothing():啥也不做。服务恢复后,将直接把nextFireTime更新至当前时间之后的下一次的执行时间,那么之前漏掉的任务将会被抛弃掉。适用于大部分场景。
      • withMisfireHandlingInstructionFireAndProceed():立即执行一次。服务恢复后立即执行一次,即将下次执行时间修改为现在,执行任务并从现在计算下一次时间。适用于部分场景。
    • SimpleScheduleBuilder有5种方案

    • 不设置:默认,智能处理,慎用

      • withMisfireHandlingInstructionIgnoreMisfires():忽略失效策略。服务恢复后,将一次性执行多次直至错过的任务全部补充。适用于部分场景。
      • withMisfireHandlingInstructionFireNow():立即执行一次,通常用于不重复执行的任务。即恢复后立即执行一次,对于不重复的任务相当于重试,对于重复的任务会立即执行一次,仅保留剩余次数而直接遗忘掉开始时间和总次数
    • withMisfireHandlingInstructionNextWithExistingCount():从当前时间之后的下次执行时间继续,错过的次数保留,不扣除

      • withMisfireHandlingInstructionNextWithRemainingCount():从当前时间之后的下次执行时间继续,错过的次数扣除
    • withMisfireHandlingInstructionNowWithExistingCount():从现在继续,错过的次数保留,不扣除。遗忘开始时间和总次数

      • withMisfireHandlingInstructionNowWithRemainingCount():从现在继续,错过的次数扣除。遗遗忘开始时间和总次数

    剩余两种ScheduleBuilder不多做描述,有兴趣的可以直接查源码,写的还是比较详细的

    若未设置misfireThreshold属性,以上方案均不会生效,依旧为智能处理,且大概率为忽略失效策略

  • pauseTrigger与pauseJob与pauseAll(resume同理)

    • pauseTrigger,即暂停触发器,那么自然任务也就不会再被触发
    • pauseJob,即暂停job,会将与自己关联的所有trigger一同暂停,resume同理
    • pauseAll,即全部暂停,实际上是暂停所有的group,并在数据库做记录,resume同理

4.可能遇到的坑

  • resume或重启项目后任务快速执行多次

    原因就是前面补充的,错过时间的处理方案,默认值是智能处理,而智能成啥样并不知道。。。建议主动设置为需要的模式

    如修改trigger构建方法如下

             //构建新的触发器
             Trigger trigger = TriggerBuilder
                     .newTrigger()
    //                    .forJob(jobDetail)//目标job,这里不设置,如果设置了将会自动触发
                     .withIdentity(quartzJob.getJobName() + "Trigger", quartzJob.getJobGroup())//设置job名和分组名
                     .usingJobData("triggerName", quartzJob.getJobName() + "Trigger")//传递的参数,可以自定义,可传递多组,以map形式传递
                     .usingJobData(jobDataMap)
                     .withSchedule(CronScheduleBuilder
                             .cronSchedule(quartzJob.getCronExpression().trim())
                             .withMisfireHandlingInstructionDoNothing()
                     )
                     .startNow()//现在就启动
    //                    .startAt(new Date())//启动时间,可以自定义,与startNow冲突
                     .build();
    
  • pauseAll之后删除,然后再建立一样的任务,任务默认为暂停状态,而不是自动启动

    pauseAll会暂停所有的triggerGroup并存储到表qrtz_paused_trigger_grps,而删除任务时并不会删除这张表里的内容(但会删除trigger)

    那么当从新建立一个一毛一样的任务的时候,quartz就会将其识别为pause状态,因而并不会启动

    至于解决办法。。。个人建议批量暂停不要用pauseAll实现,完全可以查出所有任务,然后挨个pauseJob


5.demo地址

https://gitee.com/echo_ye/demo_basic/tree/scheduleDemo

仅实现了部分功能作为样例,请按照需求自己扩展哦,有疑问或者建议欢迎联系我~

BB两句

quartz之前只在spring用过。。。本来以为就一个小组件,打算几个定时器整理在一起得了。。。结果越学越多。。不得不分离出来作为单独的文章

当然整理出来的依然只有一小部分,后面有机会再扩充整理一遍

而且讲道理quartz的注释是写的真的挺好,我这个英语渣渣跟着debug看源码也问题不大



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

上一篇:第10章《Spark Streaming》


下一篇:java 定时任务接口scheduledExecutorService