天天看點

任務排程架構Quartz用法指南(超詳細)

作者:二哥學Java
任務排程架構Quartz用法指南(超詳細)

前言

項目中遇到一個,需要 客戶自定任務啟動時間 的需求。原來一直都是在項目裡寫死一些定時器,是以沒有學習過。

很多開源的項目管理架構都已經做了Quartz的內建。我們居然連這麼常用得東西居然沒有做成子產品化,實在是不應該。

Quartz是OpenSymphony開源組織在Job scheduling領域又一個開源項目,完全由Java開發,可以用來執行定時任務,類似于java.util.Timer。但是相較于Timer, Quartz增加了很多功能:

  • 持久性作業 - 就是保持排程定時的狀态;
  • 作業管理 - 對排程作業進行有效的管理;

官方文檔:

  • http://www.quartz-scheduler.org/documentation/
  • http://www.quartz-scheduler.org/api/2.3.0/index.html

基礎使用

Quartz 的核心類有以下三部分:

  • 任務 Job : 需要實作的任務類,實作 execute() 方法,執行後完成任務。
  • 觸發器 Trigger : 包括 SimpleTrigger 和 CronTrigger。
  • 排程器 Scheduler : 任務排程器,負責基于 Trigger觸發器,來執行 Job任務。

主要關系如下:

任務排程架構Quartz用法指南(超詳細)

Demo

按照官網的 Demo,搭建一個純 maven 項目,添加依賴:

<!-- 核心包 -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.0</version>
</dependency>
<!-- 工具包 -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.3.0</version>
</dependency>
           

建立一個任務,實作了 org.quartz.Job 接口:

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("任務被執行了。。。");
    }
}
           

main 方法,建立排程器、jobDetail 執行個體、trigger 執行個體、執行:

public static void main(String[] args) throws Exception {
    // 1.建立排程器 Scheduler
    SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = factory.getScheduler();

    // 2.建立JobDetail執行個體,并與MyJob類綁定(Job執行内容)
    JobDetail job = JobBuilder.newJob(MyJob.class)
        .withIdentity("job1", "group1")
        .build();

    // 3.建構Trigger執行個體,每隔30s執行一次
    Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger1", "group1")
        .startNow()
        .withSchedule(simpleSchedule()
                      .withIntervalInSeconds(30)
                      .repeatForever())
        .build();

    // 4.執行,開啟排程器
    scheduler.scheduleJob(job, trigger);
    System.out.println(System.currentTimeMillis());
    scheduler.start();

    //主線程睡眠1分鐘,然後關閉排程器
    TimeUnit.MINUTES.sleep(1);
    scheduler.shutdown();
    System.out.println(System.currentTimeMillis());
}
           

日志列印情況:

任務排程架構Quartz用法指南(超詳細)

JobDetail

JobDetail 的作用是綁定 Job,是一個任務執行個體,它為 Job 添加了許多擴充參數。

任務排程架構Quartz用法指南(超詳細)

每次Scheduler排程執行一個Job的時候,首先會拿到對應的Job,然後建立該Job執行個體,再去執行Job中的execute()的内容,任務執行結束後,關聯的Job對象執行個體會被釋放,且會被JVM GC清除。

為什麼設計成JobDetail + Job,不直接使用Job?

JobDetail 定義的是任務資料,而真正的執行邏輯是在Job中。

這是因為任務是有可能并發執行,如果Scheduler直接使用Job,就會存在對同一個Job執行個體并發通路的問題。

而JobDetail & Job 方式,Sheduler每次執行,都會根據JobDetail建立一個新的Job執行個體,這樣就可以 規避并發通路 的問題。

JobExecutionContext

  • 當 Scheduler 調用一個 job,就會将 JobExecutionContext 傳遞給 Job 的 execute() 方法;
  • Job 能通過 JobExecutionContext 對象通路到 Quartz 運作時候的環境以及 Job 本身的明細資料。

任務實作的 execute() 方法,可以通過 context 參數擷取。

public interface Job {
    void execute(JobExecutionContext context)
        throws JobExecutionException;
}
           

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

usingJobData("tiggerDataMap", "測試傳參")
           

在 execute 方法中擷取:

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

Job 狀态參數

有狀态的 job 可以了解為多次 job調用期間可以持有一些狀态資訊,這些狀态資訊存儲在 JobDataMap 中。

而預設的無狀态 job,每次調用時都會建立一個新的 JobDataMap。

示例如下:

//多次調用 Job 的時候,将參數保留在 JobDataMap
@PersistJobDataAfterExecution
public class JobStatus implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        long count = (long) context.getJobDetail().getJobDataMap().get("count");
        System.out.println("目前執行,第" + count + "次");
        context.getJobDetail().getJobDataMap().put("count", ++count);
    }
}
           
JobDetail job = JobBuilder.newJob(JobStatus.class)
                .withIdentity("statusJob", "group1")
                .usingJobData("count", 1L)
                .build();
           

輸出結果:

目前執行,第1次
[main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
目前執行,第2次
目前執行,第3次
           

Trigger

定時啟動/關閉

Trigger 可以設定任務的開始結束時間, Scheduler 會根據參數進行觸發。

Calendar instance = Calendar.getInstance();
Date startTime = instance.getTime();
instance.add(Calendar.MINUTE, 1);
Date endTime = instance.getTime();

// 3.建構Trigger執行個體
Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("trigger1", "group1")
    // 開始時間
    .startAt(startTime)
    // 結束時間
    .endAt(endTime)
    .build();
           

在 job 中也能拿到對應的時間,并進行業務判斷

public void execute(JobExecutionContext context) throws JobExecutionException {
    System.out.println("任務執行。。。");
    System.out.println(context.getTrigger().getStartTime());
    System.out.println(context.getTrigger().getEndTime());
}
           

運作結果:

[main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.0
1633149326723
任務執行。。。
Sat Oct 02 12:35:26 CST 2021
Sat Oct 02 12:36:26 CST 2021
[main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
           

SimpleTrigger

這是比較簡單的一類觸發器,用它能實作很多基礎的應用。使用它的主要場景包括:

  • 在指定時間段内,執行一次任務

最基礎的 Trigger 不設定循環,設定開始時間。

  • 在指定時間段内,循環執行任務

在 1 基礎上加上循環間隔。可以指定 永遠循環、運作指定次數

TriggerBuilder.newTrigger()
    .withSchedule(SimpleScheduleBuilder
                  .simpleSchedule()
                  .withIntervalInSeconds(30)
                  .repeatForever())
           

withRepeatCount(count) 是重複次數,實際運作次數為 count+1

TriggerBuilder.newTrigger()
    .withSchedule(SimpleScheduleBuilder
                  .simpleSchedule()
                  .withIntervalInSeconds(30)
                  .withRepeatCount(5))
           
  • 立即開始,指定時間結束

這個,略。

CronTrigger

CronTrigger 是基于月曆的任務排程器,在實際應用中更加常用。

雖然很常用,但是知識點都一樣,隻是可以通過表達式來設定時間而已。

使用方式就是綁定排程器時換一下:

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

Cron 表達式這裡不介紹,貼個圖跳過

任務排程架構Quartz用法指南(超詳細)

SpringBoot 整合

下面內建應用截圖來自 Ruoyi 架構:

任務排程架構Quartz用法指南(超詳細)
任務排程架構Quartz用法指南(超詳細)

從上面的截圖中,可以看到這個定時任務子產品實作了:

  • cron表達式定時執行
  • 并發執行
  • 錯誤政策
  • 啟動執行、暫停執行

如果再加上:設定啟動時間、停止時間 就更好了。不過啟停時間隻是調用兩個方法而已,也就不寫了。

這一部分就主要是看RuoYi 架構代碼,然後加一點我需要用的功能。

前端部分就不寫了,全部用 swagger 代替,一些基礎字段也删除了,僅複制主體功能。

已完成代碼示例:

https://gitee.com/qianwei4712/code-of-shiva/tree/master/quartz

環境準備

從 springboot 2.4.10 開始,添加 quartz 的 maven 依賴:

<!--  Quartz 任務排程 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
           

application 配置檔案:

# 開發環境配置
server:
  # 伺服器的HTTP端口
  port: 80
  servlet:
    # 應用的通路路徑
    context-path: /
  tomcat:
    # tomcat的URI編碼
    uri-encoding: UTF-8

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&useSSL=true
    driver-class-name: com.mysql.cj.jdbc.Driver

    # HikariPool 較佳配置
    hikari:
      # 用戶端(即您)等待來自池的連接配接的最大毫秒數
      connection-timeout: 60000
      # 控制将測試連接配接的活動性的最長時間
      validation-timeout: 3000
      # 控制允許連接配接在池中保持空閑狀态的最長時間
      idle-timeout: 60000

      login-timeout: 5
      # 控制池中連接配接的最大生存期
      max-lifetime: 60000
      # 控制允許池達到的最大大小,包括空閑和使用中的連接配接
      maximum-pool-size: 10
      # 控制HikariCP嘗試在池中維護的最小空閑連接配接數
      minimum-idle: 10
      # 控制預設情況下從池獲得的連接配接是否處于隻讀模式
      read-only: false
           

Quartz 自帶有資料庫模式,腳本都是現成的:

下載下傳這個腳本:

https://gitee.com/qianwei4712/code-of-shiva/blob/master/quartz/quartz.sql

儲存任務的資料庫表:

CREATE TABLE `quartz_job` (
  `job_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任務ID',
  `job_name` varchar(64) NOT NULL DEFAULT '' COMMENT '任務名稱',
  `job_group` varchar(64) NOT NULL DEFAULT 'DEFAULT' COMMENT '任務組名',
  `invoke_target` varchar(500) NOT NULL COMMENT '調用目标字元串',
  `cron_expression` varchar(255) DEFAULT '' COMMENT 'cron執行表達式',
  `misfire_policy` varchar(20) DEFAULT '3' COMMENT '計劃執行錯誤政策(1立即執行 2執行一次 3放棄執行)',
  `concurrent` char(1) DEFAULT '1' COMMENT '是否并發執行(0允許 1禁止)',
  `status` char(1) DEFAULT '0' COMMENT '狀态(0正常 1暫停)',
  `remark` varchar(500) DEFAULT '' COMMENT '備注資訊',
  PRIMARY KEY (`job_id`,`job_name`,`job_group`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='定時任務排程表';
           

最後準備一個任務方法:

@Slf4j
@Component("mysqlJob")
public class MysqlJob {
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    public void execute(String param) {
        logger.info("執行 Mysql Job,目前時間:{},任務參數:{}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), param);
    }
}
           

核心代碼

ScheduleConfig 配置代碼類:

@Configuration
public class ScheduleConfig {

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);

        // quartz參數
        Properties prop = new Properties();
        prop.put("org.quartz.scheduler.instanceName", "shivaScheduler");
        prop.put("org.quartz.scheduler.instanceId", "AUTO");
        // 線程池配置
        prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        prop.put("org.quartz.threadPool.threadCount", "20");
        prop.put("org.quartz.threadPool.threadPriority", "5");
        // JobStore配置
        prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
        // 叢集配置
        prop.put("org.quartz.jobStore.isClustered", "true");
        prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
        prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");

        // sqlserver 啟用
        // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
        prop.put("org.quartz.jobStore.misfireThreshold", "12000");
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
        factory.setQuartzProperties(prop);

        factory.setSchedulerName("shivaScheduler");
        // 延時啟動
        factory.setStartupDelay(1);
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");
        // 可選,QuartzScheduler
        // 啟動時更新己存在的Job,這樣就不用每次修改targetObject後删除qrtz_job_details表對應記錄了
        factory.setOverwriteExistingJobs(true);
        // 設定自動啟動,預設為true
        factory.setAutoStartup(true);

        return factory;
    }
}
           

ScheduleUtils 排程工具類,這是本篇中最核心的代碼:

public class ScheduleUtils {
    /**
     * 得到quartz任務類
     *
     * @param job 執行計劃
     * @return 具體執行任務類
     */
    private static Class<? extends Job> getQuartzJobClass(QuartzJob job) {
        boolean isConcurrent = "0".equals(job.getConcurrent());
        return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
    }

    /**
     * 建構任務觸發對象
     */
    public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
        return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
    }

    /**
     * 建構任務鍵對象
     */
    public static JobKey getJobKey(Long jobId, String jobGroup) {
        return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
    }

    /**
     * 建立定時任務
     */
    public static void createScheduleJob(Scheduler scheduler, QuartzJob job) throws Exception {
        Class<? extends Job> jobClass = getQuartzJobClass(job);
        // 建構job資訊
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();

        // 表達式排程建構器
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
        cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);

        // 按新的cronExpression表達式建構一個新的trigger
        CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
                .withSchedule(cronScheduleBuilder).build();

        // 放入參數,運作時的方法可以擷取
        jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);

        // 判斷是否存在
        if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
            // 防止建立時存在資料問題 先移除,然後在執行建立操作
            scheduler.deleteJob(getJobKey(jobId, jobGroup));
        }

        scheduler.scheduleJob(jobDetail, trigger);

        // 暫停任務
        if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
            scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        }
    }

    /**
     * 設定定時任務政策
     */
    public static CronScheduleBuilder handleCronScheduleMisfirePolicy(QuartzJob job, CronScheduleBuilder cb)
            throws Exception {
        switch (job.getMisfirePolicy()) {
            case ScheduleConstants.MISFIRE_DEFAULT:
                return cb;
            case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
                return cb.withMisfireHandlingInstructionIgnoreMisfires();
            case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
                return cb.withMisfireHandlingInstructionFireAndProceed();
            case ScheduleConstants.MISFIRE_DO_NOTHING:
                return cb.withMisfireHandlingInstructionDoNothing();
            default:
                throw new Exception("The task misfire policy '" + job.getMisfirePolicy()
                        + "' cannot be used in cron schedule tasks");
        }
    }
}
           

這裡可以看到,在完成任務與觸發器的關聯後,如果是暫停狀态,會先讓排程器停止任務。

AbstractQuartzJob 抽象任務:

public abstract class AbstractQuartzJob implements Job {
    private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);

    /**
     * 線程本地變量
     */
    private static ThreadLocal<Date> threadLocal = new ThreadLocal<>();

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        QuartzJob job = new QuartzJob();
        BeanUtils.copyBeanProp(job, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
        try {
            before(context, job);
            if (job != null) {
                doExecute(context, job);
            }
            after(context, job, null);
        } catch (Exception e) {
            log.error("任務執行異常  - :", e);
            after(context, job, e);
        }
    }

    /**
     * 執行前
     *
     * @param context 工作執行上下文對象
     * @param job     系統計劃任務
     */
    protected void before(JobExecutionContext context, QuartzJob job) {
        threadLocal.set(new Date());
    }

    /**
     * 執行後
     *
     * @param context 工作執行上下文對象
     * @param sysJob  系統計劃任務
     */
    protected void after(JobExecutionContext context, QuartzJob sysJob, Exception e) {

    }

    /**
     * 執行方法,由子類重載
     *
     * @param context 工作執行上下文對象
     * @param job     系統計劃任務
     * @throws Exception 執行過程中的異常
     */
    protected abstract void doExecute(JobExecutionContext context, QuartzJob job) throws Exception;
}
           
這個類将原本 execute 方法執行的任務,下放到了子類重載的 doExecute 方法中

同時準備實作,分了允許并發和不允許并發,差别就是一個注解:

public class QuartzJobExecution extends AbstractQuartzJob {
    @Override
    protected void doExecute(JobExecutionContext context, QuartzJob job) throws Exception {
        JobInvokeUtil.invokeMethod(job);
    }
}
           
@DisallowConcurrentExecution
public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob {
    @Override
    protected void doExecute(JobExecutionContext context, QuartzJob job) throws Exception {
        JobInvokeUtil.invokeMethod(job);
    }
}
           

最後由 JobInvokeUtil 通過反射,進行實際的方法調用:

public class JobInvokeUtil {
    /**
     * 執行方法
     *
     * @param job 系統任務
     */
    public static void invokeMethod(QuartzJob job) throws Exception {
        String invokeTarget = job.getInvokeTarget();
        String beanName = getBeanName(invokeTarget);
        String methodName = getMethodName(invokeTarget);
        List<Object[]> methodParams = getMethodParams(invokeTarget);

        if (!isValidClassName(beanName)) {
            Object bean = SpringUtils.getBean(beanName);
            invokeMethod(bean, methodName, methodParams);
        } else {
            Object bean = Class.forName(beanName).newInstance();
            invokeMethod(bean, methodName, methodParams);
        }
    }

    /**
     * 調用任務方法
     *
     * @param bean         目标對象
     * @param methodName   方法名稱
     * @param methodParams 方法參數
     */
    private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
            InvocationTargetException {
        if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) {
            Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
            method.invoke(bean, getMethodParamsValue(methodParams));
        } else {
            Method method = bean.getClass().getDeclaredMethod(methodName);
            method.invoke(bean);
        }
    }

    /**
     * 校驗是否為為class包名
     *
     * @param invokeTarget 名稱
     * @return true是 false否
     */
    public static boolean isValidClassName(String invokeTarget) {
        return StringUtils.countMatches(invokeTarget, ".") > 1;
    }

    /**
     * 擷取bean名稱
     *
     * @param invokeTarget 目标字元串
     * @return bean名稱
     */
    public static String getBeanName(String invokeTarget) {
        String beanName = StringUtils.substringBefore(invokeTarget, "(");
        return StringUtils.substringBeforeLast(beanName, ".");
    }

    /**
     * 擷取bean方法
     *
     * @param invokeTarget 目标字元串
     * @return method方法
     */
    public static String getMethodName(String invokeTarget) {
        String methodName = StringUtils.substringBefore(invokeTarget, "(");
        return StringUtils.substringAfterLast(methodName, ".");
    }

    /**
     * 擷取method方法參數相關清單
     *
     * @param invokeTarget 目标字元串
     * @return method方法相關參數清單
     */
    public static List<Object[]> getMethodParams(String invokeTarget) {
        String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
        if (StringUtils.isEmpty(methodStr)) {
            return null;
        }
        String[] methodParams = methodStr.split(",");
        List<Object[]> classs = new LinkedList<>();
        for (int i = 0; i < methodParams.length; i++) {
            String str = StringUtils.trimToEmpty(methodParams[i]);
            // String字元串類型,包含'
            if (StringUtils.contains(str, "'")) {
                classs.add(new Object[]{StringUtils.replace(str, "'", ""), String.class});
            }
            // boolean布爾類型,等于true或者false
            else if (StringUtils.equals(str, "true") || StringUtils.equalsIgnoreCase(str, "false")) {
                classs.add(new Object[]{Boolean.valueOf(str), Boolean.class});
            }
            // long長整形,包含L
            else if (StringUtils.containsIgnoreCase(str, "L")) {
                classs.add(new Object[]{Long.valueOf(StringUtils.replaceIgnoreCase(str, "L", "")), Long.class});
            }
            // double浮點類型,包含D
            else if (StringUtils.containsIgnoreCase(str, "D")) {
                classs.add(new Object[]{Double.valueOf(StringUtils.replaceIgnoreCase(str, "D", "")), Double.class});
            }
            // 其他類型歸類為整形
            else {
                classs.add(new Object[]{Integer.valueOf(str), Integer.class});
            }
        }
        return classs;
    }

    /**
     * 擷取參數類型
     *
     * @param methodParams 參數相關清單
     * @return 參數類型清單
     */
    public static Class<?>[] getMethodParamsType(List<Object[]> methodParams) {
        Class<?>[] classs = new Class<?>[methodParams.size()];
        int index = 0;
        for (Object[] os : methodParams) {
            classs[index] = (Class<?>) os[1];
            index++;
        }
        return classs;
    }

    /**
     * 擷取參數值
     *
     * @param methodParams 參數相關清單
     * @return 參數值清單
     */
    public static Object[] getMethodParamsValue(List<Object[]> methodParams) {
        Object[] classs = new Object[methodParams.size()];
        int index = 0;
        for (Object[] os : methodParams) {
            classs[index] = (Object) os[0];
            index++;
        }
        return classs;
    }
}
           

啟動程式後可以看到,排程器已經啟動:

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

新增排程任務

添加任務,使用如下 json 進行請求:

{
  "concurrent": "1",
  "cronExpression": "0/10 * * * * ?",
  "invokeTarget": "mysqlJob.execute('got it!!!')",
  "jobGroup": "mysqlGroup",
  "jobId": 9,
  "jobName": "新增 mysqlJob 任務",
  "misfirePolicy": "1",
  "remark": "",
  "status": "0"
}
           
@Override
@Transactional(rollbackFor = Exception.class)
public int insertJob(QuartzJob job) throws Exception {
    // 先将任務設定為暫停狀态
    job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
    int rows = quartzMapper.insert(job);
    if (rows > 0) {
        ScheduleUtils.createScheduleJob(scheduler, job);
    }
    return rows;
}
           

先将任務設定為暫停狀态,資料庫插入成功後,在排程器建立任務。

再手動啟動任務,根據 ID 來啟動任務:

任務排程架構Quartz用法指南(超詳細)

實作代碼:

@Override
    public int changeStatus(Long jobId, String status) throws SchedulerException {
        int rows = quartzMapper.changeStatus(jobId, status);
        if (rows == 0) {
            return rows;
        }
        //更新成功,需要改排程器内任務的狀态
        //拿到整個任務
        QuartzJob job = quartzMapper.selectJobById(jobId);
        //根據狀态來啟動或者關閉
        if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) {
            rows = resumeJob(job);
        } else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) {
            rows = pauseJob(job);
        }
        return rows;
    }
           
@Override
public int resumeJob(QuartzJob job) throws SchedulerException {
    Long jobId = job.getJobId();
    String jobGroup = job.getJobGroup();
    job.setStatus(ScheduleConstants.Status.NORMAL.getValue());
    int rows = quartzMapper.updateById(job);
    if (rows > 0) {
        scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup));
    }
    return rows;
}
           

暫停任務的代碼也相同。

調用啟動後可以看到控制台列印日志:

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!!!
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!!!
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!!!
           

如果涉及到任務修改,需要在排程器先删除原有任務,重新建立排程任務。

啟動初始化任務

這部分倒是比較簡單,初始化的時候清空原有任務,重新建立就好了:

/**
  * 項目啟動時,初始化定時器 主要是防止手動修改資料庫導緻未同步到定時任務處理(注:不能手動修改資料庫ID和任務組名,否則會導緻髒資料)
  */
@PostConstruct
public void init() throws Exception {
    scheduler.clear();
    List<QuartzJob> jobList = quartzMapper.selectJobAll();
    for (QuartzJob job : jobList) {
        ScheduleUtils.createScheduleJob(scheduler, job);
    }
}
           

其他說明

并發執行

上面有并發和非并發的差別,通過 @DisallowConcurrentExecution 注解來實作阻止并發。

Quartz定時任務預設都是并發執行的,不會等待上一次任務執行完畢,隻要間隔時間到就會執行, 如果定時任執行太長,會長時間占用資源,導緻其它任務堵塞。

@DisallowConcurrentExecution 禁止并發執行多個相同定義的JobDetail, 這個注解是加在Job類上的, 但意思并不是不能同時執行多個Job, 而是不能并發執行同一個Job Definition(由JobDetail定義), 但是可以同時執行多個不同的JobDetail。

舉例說明,我們有一個Job類,叫做SayHelloJob, 并在這個Job上加了這個注解, 然後在這個Job上定義了很多個JobDetail, 如sayHelloToJoeJobDetail, sayHelloToMikeJobDetail, 那麼當scheduler啟動時, 不會并發執行多個sayHelloToJoeJobDetail或者sayHelloToMikeJobDetail, 但可以同時執行sayHelloToJoeJobDetail跟sayHelloToMikeJobDetail

@PersistJobDataAfterExecution 同樣, 也是加在Job上。表示當正常執行完Job後, JobDataMap中的資料應該被改動, 以被下一次調用時用。

當使用 @PersistJobDataAfterExecution 注解時, 為了避免并發時, 存儲資料造成混亂, 強烈建議把 @DisallowConcurrentExecution 注解也加上。

測試代碼,設定的時間間隔為3秒,但job執行時間是5秒,設定 @DisallowConcurrentExecution以 後程式會等任務執行完畢以後再去執行,否則會在3秒時再啟用新的線程執行。

阻止特定時間運作

仍然是通過排程器實作的:

//2014-8-15這一天不執行任何任務
Calendar c = new GregorianCalendar(2014, 7, 15);
cal.setDayExcluded(c, true);
scheduler.addCalendar("exclude", cal, false, false);

//...中間省略
TriggerBuilder.newTrigger().modifiedByCalendar("exclude")....           

繼續閱讀