Java
任务调度框架 Quartz 用法指南 - 图1

1、前言

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

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

官方文档:

主要关系如下:
任务调度框架 Quartz 用法指南 - 图2

3、Demo

按照官网的 Demo,搭建一个纯 Maven 项目,添加依赖:

  1. <!-- 核心包 -->
  2. <dependency>
  3. <groupId>org.quartz-scheduler</groupId>
  4. <artifactId>quartz</artifactId>
  5. <version>2.3.0</version>
  6. </dependency>
  7. <!-- 工具包 -->
  8. <dependency>
  9. <groupId>org.quartz-scheduler</groupId>
  10. <artifactId>quartz-jobs</artifactId>
  11. <version>2.3.0</version>
  12. </dependency>

新建一个任务,实现了 org.quartz.Job 接口:

  1. public class MyJob implements Job {
  2. @Override
  3. public void execute(JobExecutionContext context) throws JobExecutionException {
  4. System.out.println("任务被执行了。。。");
  5. }
  6. }

main 方法,创建调度器、jobDetail 实例、trigger 实例、执行:

  1. public static void main(String[] args) throws Exception {
  2. // 1.创建调度器 Scheduler
  3. SchedulerFactory factory = new StdSchedulerFactory();
  4. Scheduler scheduler = factory.getScheduler();
  5. // 2.创建JobDetail实例,并与MyJob类绑定(Job执行内容)
  6. JobDetail job = JobBuilder.newJob(MyJob.class)
  7. .withIdentity("job1", "group1")
  8. .build();
  9. // 3.构建Trigger实例,每隔30s执行一次
  10. Trigger trigger = TriggerBuilder.newTrigger()
  11. .withIdentity("trigger1", "group1")
  12. .startNow()
  13. .withSchedule(simpleSchedule()
  14. .withIntervalInSeconds(30)
  15. .repeatForever())
  16. .build();
  17. // 4.执行,开启调度器
  18. scheduler.scheduleJob(job, trigger);
  19. System.out.println(System.currentTimeMillis());
  20. scheduler.start();
  21. //主线程睡眠1分钟,然后关闭调度器
  22. TimeUnit.MINUTES.sleep(1);
  23. scheduler.shutdown();
  24. System.out.println(System.currentTimeMillis());
  25. }

日志打印情况:
任务调度框架 Quartz 用法指南 - 图3

4、JobDetail

JobDetail 的作用是绑定 Job,是一个任务实例,它为 Job 添加了许多扩展参数。
任务调度框架 Quartz 用法指南 - 图4
每次Scheduler调度执行一个Job的时候,首先会拿到对应的Job,然后创建该Job实例,再去执行Job中的execute()的内容,任务执行结束后,关联的Job对象实例会被释放,且会被JVM GC清除。
为什么设计成JobDetail + Job,不直接使用Job?
JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。
这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。
而JobDetail & Job 方式,Sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以 规避并发访问 的问题。

5、JobExecutionContext

  • 当 Scheduler 调用一个 job,就会将 JobExecutionContext 传递给 Job 的 execute() 方法;
  • Job 能通过 JobExecutionContext 对象访问到 Quartz 运行时候的环境以及 Job 本身的明细数据。

任务实现的 execute() 方法,可以通过 context 参数获取。

  1. public interface Job {
  2. void execute(JobExecutionContext context)
  3. throws JobExecutionException;
  4. }

在 Builder 建造过程中,可以使用如下方法:

  1. usingJobData("tiggerDataMap", "测试传参")

在 execute 方法中获取:

  1. context.getTrigger().getJobDataMap().get("tiggerDataMap");
  2. context.getJobDetail().getJobDataMap().get("tiggerDataMap");

6、Job 状态参数

有状态的 job 可以理解为多次 job调用期间可以持有一些状态信息,这些状态信息存储在 JobDataMap 中。
而默认的无状态 job,每次调用时都会创建一个新的 JobDataMap。
示例如下:

  1. //多次调用 Job 的时候,将参数保留在 JobDataMap
  2. @PersistJobDataAfterExecution
  3. public class JobStatus implements Job {
  4. @Override
  5. public void execute(JobExecutionContext context) throws JobExecutionException {
  6. long count = (long) context.getJobDetail().getJobDataMap().get("count");
  7. System.out.println("当前执行,第" + count + "次");
  8. context.getJobDetail().getJobDataMap().put("count", ++count);
  9. }
  10. }
  11. JobDetail job = JobBuilder.newJob(JobStatus.class)
  12. .withIdentity("statusJob", "group1")
  13. .usingJobData("count", 1L)
  14. .build();

输出结果:

  1. 当前执行,第1
  2. [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
  3. 当前执行,第2
  4. 当前执行,第3

7、Trigger

定时启动/关闭

Trigger 可以设置任务的开始结束时间, Scheduler 会根据参数进行触发。

  1. Calendar instance = Calendar.getInstance();
  2. Date startTime = instance.getTime();
  3. instance.add(Calendar.MINUTE, 1);
  4. Date endTime = instance.getTime();
  5. // 3.构建Trigger实例
  6. Trigger trigger = TriggerBuilder.newTrigger()
  7. .withIdentity("trigger1", "group1")
  8. // 开始时间
  9. .startAt(startTime)
  10. // 结束时间
  11. .endAt(endTime)
  12. .build();

在 job 中也能拿到对应的时间,并进行业务判断

  1. public void execute(JobExecutionContext context) throws JobExecutionException {
  2. System.out.println("任务执行。。。");
  3. System.out.println(context.getTrigger().getStartTime());
  4. System.out.println(context.getTrigger().getEndTime());
  5. }

运行结果:

  1. [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.0
  2. 1633149326723
  3. 任务执行。。。
  4. Sat Oct 02 12:35:26 CST 2021
  5. Sat Oct 02 12:36:26 CST 2021
  6. [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.

8、SimpleTrigger

这是比较简单的一类触发器,用它能实现很多基础的应用。使用它的主要场景包括:

  • 在指定时间段内,执行一次任务最基础的 Trigger 不设置循环,设置开始时间。
  • 在指定时间段内,循环执行任务在 1 基础上加上循环间隔。可以指定 永远循环、运行指定次数

    1. TriggerBuilder.newTrigger()
    2. .withSchedule(SimpleScheduleBuilder
    3. .simpleSchedule()
    4. .withIntervalInSeconds(30)
    5. .repeatForever())

    withRepeatCount(count) 是重复次数,实际运行次数为 count+1

    1. TriggerBuilder.newTrigger()
    2. .withSchedule(SimpleScheduleBuilder
    3. .simpleSchedule()
    4. .withIntervalInSeconds(30)
    5. .withRepeatCount(5))
  • 立即开始,指定时间结束这个,略。

    9、CronTrigger

    CronTrigger 是基于日历的任务调度器,在实际应用中更加常用。
    虽然很常用,但是知识点都一样,只是可以通过表达式来设置时间而已。
    使用方式就是绑定调度器时换一下:

    1. TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ?"))

    Cron 表达式这里不介绍,贴个图跳过
    任务调度框架 Quartz 用法指南 - 图5
    顺便推荐一个非常好用的Cron 表达式在线生成,反解析的工具:www.matools.com/cron 非常好用,点几下,就能得到自己想要的cron表达式;

    10、SpringBoot 整合

    下面集成应用截图来自 Ruoyi 框架:
    任务调度框架 Quartz 用法指南 - 图6
    任务调度框架 Quartz 用法指南 - 图7
    从上面的截图中,可以看到这个定时任务模块实现了:

  • cron表达式定时执行

  • 并发执行
  • 错误策略
  • 启动执行、暂停执行

如果再加上:设置启动时间、停止时间 就更好了。不过启停时间只是调用两个方法而已,也就不写了。
这一部分就主要是看 RuoYi 框架 代码,然后加一点需要用的功能。
前端部分就不写了,全部用 swagger 代替,一些基础字段也删除了,仅复制主体功能。
已完成代码示例:https://gitee.com/qianwei4712/code-of-shiva/tree/master/quartz

11环境准备

从 springboot 2.4.10 开始,添加 quartz 的 maven 依赖:

  1. <!-- Quartz 任务调度 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-quartz</artifactId>
  5. </dependency>

application 配置文件:

  1. # 开发环境配置
  2. server:
  3. # 服务器的HTTP端口
  4. port: 80
  5. servlet:
  6. # 应用的访问路径
  7. context-path: /
  8. tomcat:
  9. # tomcat的URI编码
  10. uri-encoding: UTF-8
  11. spring:
  12. datasource:
  13. username: root
  14. password: root
  15. url: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&useSSL=true
  16. driver-class-name: com.mysql.cj.jdbc.Driver
  17. # HikariPool 较佳配置
  18. hikari:
  19. # 客户端(即您)等待来自池的连接的最大毫秒数
  20. connection-timeout: 60000
  21. # 控制将测试连接的活动性的最长时间
  22. validation-timeout: 3000
  23. # 控制允许连接在池中保持空闲状态的最长时间
  24. idle-timeout: 60000
  25. login-timeout: 5
  26. # 控制池中连接的最大生存期
  27. max-lifetime: 60000
  28. # 控制允许池达到的最大大小,包括空闲和使用中的连接
  29. maximum-pool-size: 10
  30. # 控制HikariCP尝试在池中维护的最小空闲连接数
  31. minimum-idle: 10
  32. # 控制默认情况下从池获得的连接是否处于只读模式
  33. read-only: false

Quartz 自带有数据库模式,脚本都是现成的:
下载这个脚本:https://gitee.com/qianwei4712/code-of-shiva/blob/master/quartz/quartz.sql
保存任务的数据库表:

  1. CREATE TABLE `quartz_job` (
  2. `job_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务ID',
  3. `job_name` varchar(64) NOT NULL DEFAULT '' COMMENT '任务名称',
  4. `job_group` varchar(64) NOT NULL DEFAULT 'DEFAULT' COMMENT '任务组名',
  5. `invoke_target` varchar(500) NOT NULL COMMENT '调用目标字符串',
  6. `cron_expression` varchar(255) DEFAULT '' COMMENT 'cron执行表达式',
  7. `misfire_policy` varchar(20) DEFAULT '3' COMMENT '计划执行错误策略(1立即执行 2执行一次 3放弃执行)',
  8. `concurrent` char(1) DEFAULT '1' COMMENT '是否并发执行(0允许 1禁止)',
  9. `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1暂停)',
  10. `remark` varchar(500) DEFAULT '' COMMENT '备注信息',
  11. PRIMARY KEY (`job_id`,`job_name`,`job_group`)
  12. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='定时任务调度表';

最后准备一个任务方法:

  1. @Slf4j
  2. @Component("mysqlJob")
  3. public class MysqlJob {
  4. protected final Logger logger = LoggerFactory.getLogger(this.getClass());
  5. public void execute(String param) {
  6. logger.info("执行 Mysql Job,当前时间:{},任务参数:{}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), param);
  7. }
  8. }

12、核心代码

ScheduleConfig 配置代码类:

  1. @Configuration
  2. public class ScheduleConfig {
  3. @Bean
  4. public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
  5. SchedulerFactoryBean factory = new SchedulerFactoryBean();
  6. factory.setDataSource(dataSource);
  7. // quartz参数
  8. Properties prop = new Properties();
  9. prop.put("org.quartz.scheduler.instanceName", "shivaScheduler");
  10. prop.put("org.quartz.scheduler.instanceId", "AUTO");
  11. // 线程池配置
  12. prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
  13. prop.put("org.quartz.threadPool.threadCount", "20");
  14. prop.put("org.quartz.threadPool.threadPriority", "5");
  15. // JobStore配置
  16. prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
  17. // 集群配置
  18. prop.put("org.quartz.jobStore.isClustered", "true");
  19. prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
  20. prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
  21. prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");
  22. // sqlserver 启用
  23. // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
  24. prop.put("org.quartz.jobStore.misfireThreshold", "12000");
  25. prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
  26. factory.setQuartzProperties(prop);
  27. factory.setSchedulerName("shivaScheduler");
  28. // 延时启动
  29. factory.setStartupDelay(1);
  30. factory.setApplicationContextSchedulerContextKey("applicationContextKey");
  31. // 可选,QuartzScheduler
  32. // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
  33. factory.setOverwriteExistingJobs(true);
  34. // 设置自动启动,默认为true
  35. factory.setAutoStartup(true);
  36. return factory;
  37. }
  38. }

ScheduleUtils 调度工具类,这是本篇中最核心的代码:

  1. public class ScheduleUtils {
  2. /**
  3. * 得到quartz任务类
  4. *
  5. * @param job 执行计划
  6. * @return 具体执行任务类
  7. */
  8. private static Class<? extends Job> getQuartzJobClass(QuartzJob job) {
  9. boolean isConcurrent = "0".equals(job.getConcurrent());
  10. return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
  11. }
  12. /**
  13. * 构建任务触发对象
  14. */
  15. public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
  16. return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
  17. }
  18. /**
  19. * 构建任务键对象
  20. */
  21. public static JobKey getJobKey(Long jobId, String jobGroup) {
  22. return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
  23. }
  24. /**
  25. * 创建定时任务
  26. */
  27. public static void createScheduleJob(Scheduler scheduler, QuartzJob job) throws Exception {
  28. Class<? extends Job> jobClass = getQuartzJobClass(job);
  29. // 构建job信息
  30. Long jobId = job.getJobId();
  31. String jobGroup = job.getJobGroup();
  32. JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
  33. // 表达式调度构建器
  34. CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
  35. cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
  36. // 按新的cronExpression表达式构建一个新的trigger
  37. CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
  38. .withSchedule(cronScheduleBuilder).build();
  39. // 放入参数,运行时的方法可以获取
  40. jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
  41. // 判断是否存在
  42. if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
  43. // 防止创建时存在数据问题 先移除,然后在执行创建操作
  44. scheduler.deleteJob(getJobKey(jobId, jobGroup));
  45. }
  46. scheduler.scheduleJob(jobDetail, trigger);
  47. // 暂停任务
  48. if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
  49. scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
  50. }
  51. }
  52. /**
  53. * 设置定时任务策略
  54. */
  55. public static CronScheduleBuilder handleCronScheduleMisfirePolicy(QuartzJob job, CronScheduleBuilder cb)
  56. throws Exception {
  57. switch (job.getMisfirePolicy()) {
  58. case ScheduleConstants.MISFIRE_DEFAULT:
  59. return cb;
  60. case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
  61. return cb.withMisfireHandlingInstructionIgnoreMisfires();
  62. case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
  63. return cb.withMisfireHandlingInstructionFireAndProceed();
  64. case ScheduleConstants.MISFIRE_DO_NOTHING:
  65. return cb.withMisfireHandlingInstructionDoNothing();
  66. default:
  67. throw new Exception("The task misfire policy '" + job.getMisfirePolicy()
  68. + "' cannot be used in cron schedule tasks");
  69. }
  70. }
  71. }

这里可以看到,在完成任务与触发器的关联后,如果是暂停状态,会先让调度器停止任务。
AbstractQuartzJob 抽象任务:

  1. public abstract class AbstractQuartzJob implements Job {
  2. private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);
  3. /**
  4. * 线程本地变量
  5. */
  6. private static ThreadLocal<Date> threadLocal = new ThreadLocal<>();
  7. @Override
  8. public void execute(JobExecutionContext context) throws JobExecutionException {
  9. QuartzJob job = new QuartzJob();
  10. BeanUtils.copyBeanProp(job, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
  11. try {
  12. before(context, job);
  13. if (job != null) {
  14. doExecute(context, job);
  15. }
  16. after(context, job, null);
  17. } catch (Exception e) {
  18. log.error("任务执行异常 - :", e);
  19. after(context, job, e);
  20. }
  21. }
  22. /**
  23. * 执行前
  24. *
  25. * @param context 工作执行上下文对象
  26. * @param job 系统计划任务
  27. */
  28. protected void before(JobExecutionContext context, QuartzJob job) {
  29. threadLocal.set(new Date());
  30. }
  31. /**
  32. * 执行后
  33. *
  34. * @param context 工作执行上下文对象
  35. * @param sysJob 系统计划任务
  36. */
  37. protected void after(JobExecutionContext context, QuartzJob sysJob, Exception e) {
  38. }
  39. /**
  40. * 执行方法,由子类重载
  41. *
  42. * @param context 工作执行上下文对象
  43. * @param job 系统计划任务
  44. * @throws Exception 执行过程中的异常
  45. */
  46. protected abstract void doExecute(JobExecutionContext context, QuartzJob job) throws Exception;
  47. }

这个类将原本 execute 方法执行的任务,下放到了子类重载的 doExecute 方法中
同时准备实现,分了允许并发和不允许并发,差别就是一个注解:

  1. public class QuartzJobExecution extends AbstractQuartzJob {
  2. @Override
  3. protected void doExecute(JobExecutionContext context, QuartzJob job) throws Exception {
  4. JobInvokeUtil.invokeMethod(job);
  5. }
  6. }
  7. @DisallowConcurrentExecution
  8. public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob {
  9. @Override
  10. protected void doExecute(JobExecutionContext context, QuartzJob job) throws Exception {
  11. JobInvokeUtil.invokeMethod(job);
  12. }
  13. }

最后由 JobInvokeUtil 通过反射,进行实际的方法调用:

  1. public class JobInvokeUtil {
  2. /**
  3. * 执行方法
  4. *
  5. * @param job 系统任务
  6. */
  7. public static void invokeMethod(QuartzJob job) throws Exception {
  8. String invokeTarget = job.getInvokeTarget();
  9. String beanName = getBeanName(invokeTarget);
  10. String methodName = getMethodName(invokeTarget);
  11. List<Object[]> methodParams = getMethodParams(invokeTarget);
  12. if (!isValidClassName(beanName)) {
  13. Object bean = SpringUtils.getBean(beanName);
  14. invokeMethod(bean, methodName, methodParams);
  15. } else {
  16. Object bean = Class.forName(beanName).newInstance();
  17. invokeMethod(bean, methodName, methodParams);
  18. }
  19. }
  20. /**
  21. * 调用任务方法
  22. *
  23. * @param bean 目标对象
  24. * @param methodName 方法名称
  25. * @param methodParams 方法参数
  26. */
  27. private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
  28. throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
  29. InvocationTargetException {
  30. if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) {
  31. Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
  32. method.invoke(bean, getMethodParamsValue(methodParams));
  33. } else {
  34. Method method = bean.getClass().getDeclaredMethod(methodName);
  35. method.invoke(bean);
  36. }
  37. }
  38. /**
  39. * 校验是否为为class包名
  40. *
  41. * @param invokeTarget 名称
  42. * @return true是 false否
  43. */
  44. public static boolean isValidClassName(String invokeTarget) {
  45. return StringUtils.countMatches(invokeTarget, ".") > 1;
  46. }
  47. /**
  48. * 获取bean名称
  49. *
  50. * @param invokeTarget 目标字符串
  51. * @return bean名称
  52. */
  53. public static String getBeanName(String invokeTarget) {
  54. String beanName = StringUtils.substringBefore(invokeTarget, "(");
  55. return StringUtils.substringBeforeLast(beanName, ".");
  56. }
  57. /**
  58. * 获取bean方法
  59. *
  60. * @param invokeTarget 目标字符串
  61. * @return method方法
  62. */
  63. public static String getMethodName(String invokeTarget) {
  64. String methodName = StringUtils.substringBefore(invokeTarget, "(");
  65. return StringUtils.substringAfterLast(methodName, ".");
  66. }
  67. /**
  68. * 获取method方法参数相关列表
  69. *
  70. * @param invokeTarget 目标字符串
  71. * @return method方法相关参数列表
  72. */
  73. public static List<Object[]> getMethodParams(String invokeTarget) {
  74. String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
  75. if (StringUtils.isEmpty(methodStr)) {
  76. return null;
  77. }
  78. String[] methodParams = methodStr.split(",");
  79. List<Object[]> classs = new LinkedList<>();
  80. for (int i = 0; i < methodParams.length; i++) {
  81. String str = StringUtils.trimToEmpty(methodParams[i]);
  82. // String字符串类型,包含'
  83. if (StringUtils.contains(str, "'")) {
  84. classs.add(new Object[]{StringUtils.replace(str, "'", ""), String.class});
  85. }
  86. // boolean布尔类型,等于true或者false
  87. else if (StringUtils.equals(str, "true") || StringUtils.equalsIgnoreCase(str, "false")) {
  88. classs.add(new Object[]{Boolean.valueOf(str), Boolean.class});
  89. }
  90. // long长整形,包含L
  91. else if (StringUtils.containsIgnoreCase(str, "L")) {
  92. classs.add(new Object[]{Long.valueOf(StringUtils.replaceIgnoreCase(str, "L", "")), Long.class});
  93. }
  94. // double浮点类型,包含D
  95. else if (StringUtils.containsIgnoreCase(str, "D")) {
  96. classs.add(new Object[]{Double.valueOf(StringUtils.replaceIgnoreCase(str, "D", "")), Double.class});
  97. }
  98. // 其他类型归类为整形
  99. else {
  100. classs.add(new Object[]{Integer.valueOf(str), Integer.class});
  101. }
  102. }
  103. return classs;
  104. }
  105. /**
  106. * 获取参数类型
  107. *
  108. * @param methodParams 参数相关列表
  109. * @return 参数类型列表
  110. */
  111. public static Class<?>[] getMethodParamsType(List<Object[]> methodParams) {
  112. Class<?>[] classs = new Class<?>[methodParams.size()];
  113. int index = 0;
  114. for (Object[] os : methodParams) {
  115. classs[index] = (Class<?>) os[1];
  116. index++;
  117. }
  118. return classs;
  119. }
  120. /**
  121. * 获取参数值
  122. *
  123. * @param methodParams 参数相关列表
  124. * @return 参数值列表
  125. */
  126. public static Object[] getMethodParamsValue(List<Object[]> methodParams) {
  127. Object[] classs = new Object[methodParams.size()];
  128. int index = 0;
  129. for (Object[] os : methodParams) {
  130. classs[index] = (Object) os[0];
  131. index++;
  132. }
  133. return classs;
  134. }
  135. }

启动程序后可以看到,调度器已经启动:

  1. 2021-10-06 16:26:05.162 INFO 10764 --- [shivaScheduler]] o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now, after delay of 1 seconds
  2. 2021-10-06 16:26:05.306 INFO 10764 --- [shivaScheduler]] org.quartz.core.QuartzScheduler : Scheduler shivaScheduler_$_DESKTOP-OKMJ1351633508761366 started.

13、新增调度任务

添加任务,使用如下 json 进行请求:

  1. {
  2. "concurrent": "1",
  3. "cronExpression": "0/10 * * * * ?",
  4. "invokeTarget": "mysqlJob.execute('got it!!!')",
  5. "jobGroup": "mysqlGroup",
  6. "jobId": 9,
  7. "jobName": "新增 mysqlJob 任务",
  8. "misfirePolicy": "1",
  9. "remark": "",
  10. "status": "0"
  11. }
  1. @Override
  2. @Transactional(rollbackFor = Exception.class)
  3. public int insertJob(QuartzJob job) throws Exception {
  4. // 先将任务设置为暂停状态
  5. job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
  6. int rows = quartzMapper.insert(job);
  7. if (rows > 0) {
  8. ScheduleUtils.createScheduleJob(scheduler, job);
  9. }
  10. return rows;
  11. }

先将任务设置为暂停状态,数据库插入成功后,在调度器创建任务。
再手动启动任务,根据 ID 来启动任务:
任务调度框架 Quartz 用法指南 - 图8
实现代码:

  1. @Override
  2. public int changeStatus(Long jobId, String status) throws SchedulerException {
  3. int rows = quartzMapper.changeStatus(jobId, status);
  4. if (rows == 0) {
  5. return rows;
  6. }
  7. //更新成功,需要改调度器内任务的状态
  8. //拿到整个任务
  9. QuartzJob job = quartzMapper.selectJobById(jobId);
  10. //根据状态来启动或者关闭
  11. if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) {
  12. rows = resumeJob(job);
  13. } else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) {
  14. rows = pauseJob(job);
  15. }
  16. return rows;
  17. }
  18. @Override
  19. public int resumeJob(QuartzJob job) throws SchedulerException {
  20. Long jobId = job.getJobId();
  21. String jobGroup = job.getJobGroup();
  22. job.setStatus(ScheduleConstants.Status.NORMAL.getValue());
  23. int rows = quartzMapper.updateById(job);
  24. if (rows > 0) {
  25. scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup));
  26. }
  27. return rows;
  28. }

暂停任务的代码也相同。
调用启动后可以看到控制台打印日志:

  1. 2021-10-06 20:36:30.018 INFO 8536 --- [eduler_Worker-3] cn.shiva.quartz.job.MysqlJob : 执行 Mysql Job,当前时间:2021-10-06 20:36:30,任务参数:got it!!!
  2. 2021-10-06 20:36:40.016 INFO 8536 --- [eduler_Worker-4] cn.shiva.quartz.job.MysqlJob : 执行 Mysql Job,当前时间:2021-10-06 20:36:40,任务参数:got it!!!
  3. 2021-10-06 20:36:50.017 INFO 8536 --- [eduler_Worker-5] cn.shiva.quartz.job.MysqlJob : 执行 Mysql Job,当前时间:2021-10-06 20:36:50,任务参数:got it!!!

如果涉及到任务修改,需要在调度器先删除原有任务,重新创建调度任务。

启动初始化任务

这部分倒是比较简单,初始化的时候清空原有任务,重新创建就好了:

  1. /**
  2. * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据)
  3. */
  4. @PostConstruct
  5. public void init() throws Exception {
  6. scheduler.clear();
  7. List<QuartzJob> jobList = quartzMapper.selectJobAll();
  8. for (QuartzJob job : jobList) {
  9. ScheduleUtils.createScheduleJob(scheduler, job);
  10. }
  11. }

14、其他说明

并发执行

上面有并发和非并发的区别,通过 @DisallowConcurrentExecution 注解来实现阻止并发。
Quartz定时任务默认都是并发执行的,不会等待上一次任务执行完毕,只要间隔时间到就会执行,如果定时任执行太长,会长时间占用资源,导致其它任务堵塞。
@DisallowConcurrentExecution 禁止并发执行多个相同定义的JobDetail,这个注解是加在Job类上的,但意思并不是不能同时执行多个Job,而是不能并发执行同一个Job Definition(由JobDetail定义),但是可以同时执行多个不同的JobDetail。
举例说明,有一个Job类,叫做SayHelloJob,并在这个Job上加了这个注解,然后在这个Job上定义了很多个JobDetail,如sayHelloToJoeJobDetailsayHelloToMikeJobDetail,那么当scheduler启动时,不会并发执行多个sayHelloToJoeJobDetail或者sayHelloToMikeJobDetail,但可以同时执行sayHelloToJoeJobDetailsayHelloToMikeJobDetail
@PersistJobDataAfterExecution 同样,也是加在Job上。表示当正常执行完Job后,JobDataMap中的数据应该被改动,以被下一次调用时用。
当使用 @PersistJobDataAfterExecution 注解时,为了避免并发时,存储数据造成混乱,强烈建议把 @DisallowConcurrentExecution 注解也加上。
测试代码,设定的时间间隔为3秒,但job执行时间是5秒,设置 @DisallowConcurrentExecution以 后程序会等任务执行完毕以后再去执行,否则会在3秒时再启用新的线程执行。

阻止特定时间运行

仍然是通过调度器实现的:

  1. //2014-8-15这一天不执行任何任务
  2. Calendar c = new GregorianCalendar(2014, 7, 15);
  3. cal.setDayExcluded(c, true);
  4. scheduler.addCalendar("exclude", cal, false, false);
  5. //...中间省略
  6. TriggerBuilder.newTrigger().modifiedByCalendar("exclude")....