SpringBoot集成Quartz定时任务(持久化到数据库)

前言

现在大多数项目都使用了springboot,所以本文主要讲springboot与quartz的完美整合,简化配置、持久化数据并自定义quartz数据源。

正文

一、增加依赖

我们使用的spring-boot-starter-quartz,所以不用显示指定版本号:

<!--quartz相关依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

二、yml配置信息

quartz:
  job-store-type: jdbc
  jdbc:
    initialize-schema: never
  properties:
    org:
      quartz:
        scheduler:
          #在集群中每个实例都必须有一个唯一的instanceId,但是应该有一个相同的instanceName
          instanceId: AUTO
          instanceName: um-scheduler
          skipUpdateCheck: true #是否跳过Quartz版本更新检查。如果检查并且找到更新,则会在Quartz的日志中报告它。生产部署要禁止
        jobStore:
          acquireTriggersWithinLock: true #获取trigger的时候是否上锁,默认false采用乐观锁,但有可能出现ABA导致重复调度
          #此存储机制用于Quartz独立于应用容器的事务管理,如果是Tomcat容器管理的数据源,那我们定义的事物也不会传播给Quartz框架内部。
          #通俗的讲就是不管我们的Service服务本身业务代码是否执行成功,只要代码中调用了Quartz API的数据库操作,那任务状态就永久持久化了,
          #就算业务代码抛出运行时异常任务状态也不会回滚到之前的状态。与之相反的是JobStoreCMT。
          class: org.quartz.impl.jdbcjobstore.JobStoreTX
          driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate #JDBC代理类
          useProperties: true #让JDBCJobStore将JobDataMaps中的所有值都作为字符串,因此可以作为键值对存储而不是在BLOB列中以其序列化形式存储,从而避免序列化的版本问题
          tablePrefix: QRTZ_ #数据库表前缀
          misfireThreshold: 60_000 #超过这个时间还未触发的trigger,就被认为发生了misfire,默认60s。job成功触发叫fire,misfire就是未成功触发。
          isClustered: true #是否开启群集,集群模式需要在多台服务器上做时间同步或者使用zookeeper去解决
          clusterCheckinInterval: 20_000 #定义了Scheduler实例检入到数据库中的频率(单位:毫秒)。
        threadPool:
          class: org.quartz.simpl.SimpleThreadPool #SimpleThreadPool这个线程池只是简单地在它的池中保持固定数量的线程,不增长也不缩小。但是它非常健壮且经过良好的测试,差不多每个Quartz用户都使用这个池
          threadCount: 10 #最大线程数,意味着最多有多少个job可以同时执行
          threadPriority: 5 #线程优先级
          threadsInheritContextClassLoaderOfInitializingThread: true #线程上下文类加载器是否继承自初始线程的加载器
        startup-delay: 60 #延时启动,要有足够长的时间让你的应用先启动完成后再让Scheduler启动(单位秒)
        overwrite-existing-jobs: true #是否每次系统运行都会清空数据库中的Job信息,重新进行初始化

数据源配置:

spring: 
  # 数据库配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      datasource:
        master:
          url: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: xxx
        quartz:
          url: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root

自定生成表结构,需要配置如下信息:

spring.quartz.jdbc.initialize-schema: always
spring.quartz.job-store-type: jdbc

项目启动后生成的表信息:

 

三、定时任务逻辑封装

1.QuartzConfig定时任务配置类。

mport org.quartz.spi.JobFactory;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

import javax.sql.DataSource;
import java.io.IOException;

/**
 * @version v1.0
 * @description:
 * @author: 47 on 2020/4/9 14:32
 */
@Configuration
public class QuartzConfig {


    public void setSchedulerFactoryBeanProperties(SchedulerFactoryBean schedulerFactoryBean, DataSource dataSource, JobFactory jobFactory) throws IOException {
        schedulerFactoryBean.setJobFactory(jobFactory);
        //设置数据源
        schedulerFactoryBean.setDataSource(dataSource);
    }

    /**
     * 自定义JobFactory,以便Job类里可以使用Spring类注入
     */
    @Bean
    public JobFactory jobFactory() {
        return new QuartzSpringBeanJobFactory();
    }


    private class QuartzSpringBeanJobFactory extends SpringBeanJobFactory {

        private AutowireCapableBeanFactory beanFactory;

        @Override
        public void setApplicationContext(ApplicationContext context) {
            beanFactory = context.getAutowireCapableBeanFactory();
        }

        @Override
        protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
            final Object job = super.createJobInstance(bundle);
            beanFactory.autowireBean(job);
            return job;
        }
    }
}

2.定义SchedulerJob计划任务类

/**
 * @version v1.0
 * @description:
 * @author: 47 on 2020/4/9 14:32
 */
public class SchedulerJob {

    private String name;
    private String group;
    private String cron;
    private String jobClass;
    private String desc;
    //间隔时长
    private Long interval;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGroup() {
        return group;
    }

    public void setGroup(String group) {
        this.group = group;
    }

    public String getCron() {
        return cron;
    }

    public void setCron(String cron) {
        this.cron = cron;
    }

    public String getJobClass() {
        return jobClass;
    }

    public void setJobClass(String jobClass) {
        this.jobClass = jobClass;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public Long getInterval() {
        return interval;
    }

    public void setInterval(Long interval) {
        this.interval = interval;
    }
}

3.定义SchedulerJobs (用来主要注入配置文件里多个定时任务)

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @version v1.0
 * @description:
 * @author: 47 on 2020/4/9 14:26
 */
@Component
@ConfigurationProperties(prefix = "quartz")
public class SchedulerJobs {

    private List<SchedulerJob> jobs;

    public List<SchedulerJob> getJobs() {
        return jobs;
    }

    public void setJobs(List<SchedulerJob> jobs) {
        this.jobs = jobs;
    }
}

4.封装定时任务的方法 SchedulerManager 

import com.gamer.um.quartz.configure.SchedulerJob;
import com.gamer.um.quartz.jobs.MonitorCronJob;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @version v1.0
 * @description:
 * @author: 47 on 2020/4/9 11:37
 */
@Component
public class SchedulerManager {

    private final static Logger logger = LoggerFactory.getLogger(SchedulerManager.class);


    @Autowired
    private Scheduler scheduler;

    /**
     * 激活任务
     * @param schedulerJob
     */
    public void activeJob(SchedulerJob schedulerJob){
        JobKey jobKey = JobKey.jobKey(schedulerJob.getName(), schedulerJob.getGroup());
        try {
            if (scheduler.checkExists(jobKey) && !MonitorCronJob.JOB_NAME.equals(schedulerJob.getName())) {
                updateJob(schedulerJob);
            }else {
                createJob(schedulerJob);
            }
        } catch (SchedulerException e) {
            logger.error("activeJob {}", e);
        }
    }

    /**
     * 创建任务并加入调度
     * @param schedulerJob
     */
    public void createJob(SchedulerJob schedulerJob){
        JobKey jobKey = JobKey.jobKey(schedulerJob.getName(), schedulerJob.getGroup());
        try {
            if (scheduler.checkExists(jobKey)) {
                return;
            }
            Class<?> clazz = Class.forName(schedulerJob.getJobClass());
            JobDetail jobDetail = getJobDetail(schedulerJob, (Class<Job>) clazz);
            Trigger cronTrigger = getCronTrigger(schedulerJob);
            //加入调度器
            scheduler.scheduleJob(jobDetail, cronTrigger);
        } catch (ClassNotFoundException | SchedulerException e) {
            logger.error("createJob {}", e);
        }
    }

    /**
     * 更新任务触发器
     * @param schedulerJob
     */
    public void updateJob(SchedulerJob schedulerJob){
        TriggerKey triggerKey = TriggerKey.triggerKey(schedulerJob.getName(), schedulerJob.getGroup());
        try {
            Trigger trigger = scheduler.getTrigger(triggerKey);
            if (trigger == null) {
                return;
            }
            JobKey jobKey = trigger.getJobKey();
            //查询cron
            String oldCron = ((CronTrigger)trigger).getCronExpression();
            //没有变化则返回
            if (oldCron.equals(schedulerJob.getCron())){
                return;
            }
            Trigger cronTrigger = getCronTrigger(schedulerJob);
            //加入调度器
            scheduler.rescheduleJob(triggerKey, cronTrigger);
        } catch (SchedulerException e) {
            logger.error("updateJob {}", e);
        }
    }

    public void deleteJobs(List<JobKey> jobKeys) {
        try {
            scheduler.deleteJobs(jobKeys);
        } catch (SchedulerException e) {
            logger.error("deleteJobs {}", e);
        }
    }

    /**
     * 创建任务
     * @param schedulerJob
     * @param clazz
     * @return
     */
    private JobDetail getJobDetail(SchedulerJob schedulerJob, Class<Job> clazz) {
        return JobBuilder.newJob()
                .ofType(clazz)
                .withIdentity(schedulerJob.getName(), schedulerJob.getGroup())
                .withDescription(schedulerJob.getDesc())
                .build();
    }

    /**
     * 创建触发器
     * @param schedulerJob
     * @return
     */
    private Trigger getCronTrigger(SchedulerJob schedulerJob) {
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(schedulerJob.getCron());
        if (!MonitorCronJob.JOB_NAME.equals(schedulerJob.getName())){
            //任务错过执行策略,以错过的第一个频率时间立刻开始执行,重做错过的所有频率周期后,当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行
            cronScheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires();
        }
        return TriggerBuilder.newTrigger()
                .withIdentity(schedulerJob.getName(), schedulerJob.getGroup())
                .withDescription(schedulerJob.getDesc())
                .withSchedule(cronScheduleBuilder)
                .build();
    }

}

5.监控其他定时任务的总任务MonitorCronJob(用于监控cron的更新)

package com.gamer.um.quartz.jobs;

import com.gamer.um.quartz.SchedulerManager;
import com.gamer.um.quartz.configure.SchedulerJob;
import com.gamer.um.quartz.configure.SchedulerJobs;
import com.gamer.um.quartz.utils.LicUtil;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.refresh.ContextRefresher;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @version v1.0
 * @description:
 * @author: 47 on 2020/4/9 15:07
 */
public class MonitorCronJob implements Job {

    private final static Logger logger = LoggerFactory.getLogger(MonitorCronJob.class);

    public static final String JOB_NAME = "monitor_cron";
    public static final String GROUP_NAME = "monitor";
    public static final String CRON = "0 0/10 * * * ?";
    public static final String DESC = "监控cron更新";

    @Autowired
    private SchedulerManager schedulerManager;
    @Autowired
    private SchedulerJobs schedulerJobs;
    @Autowired
    private ContextRefresher contextRefresher;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        //重新加载配置
        contextRefresher.refresh();
        Set<JobKey> oldJobKeys = null;
        try {
            oldJobKeys = jobExecutionContext.getScheduler().getJobKeys(GroupMatcher.anyJobGroup());
        } catch (SchedulerException e) {
            logger.error("MonitorCronJob {}", e);
        }

        List<String> newJobKeys = new ArrayList<>();
        for (SchedulerJob job : schedulerJobs.getJobs()) {
            //过滤掉monitor_cron任务
            if (job.getName().equals(JOB_NAME)) {
                continue;
            }
            newJobKeys.add(job.getName());
            logger.info("job【{}】,cron【{}】", job.getName(), job.getCron());
            schedulerManager.activeJob(job);
        }
        if (oldJobKeys == null) {
            return;
        }
        //删除没有配置的任务
        List<JobKey> shouldDeleteJobKeys = oldJobKeys.stream()
                .filter(jobKey -> !JOB_NAME.equals(jobKey.getName()) && !newJobKeys.contains(jobKey.getName()))
                .collect(Collectors.toList());
        logger.info("delete jobs {}", shouldDeleteJobKeys);
        schedulerManager.deleteJobs(shouldDeleteJobKeys);
    }
}

6.配置一个定时任务(以后其他都不管,直接在配置文件里新加其他的定时任务即可)

#定时任务
quartz:
  jobs:
    - name: myName #(随便取任务名)
      group: collect
      cron: 0 0/5 * * * ? *
      jobClass: com.gamer.me.quartz.jobs.MyJob #(自己的定时任务的执行类,也就是你写业务代码的类)
      desc: 我的任务

7.设置启动项目就初始化定时任务(主要是上面的监控cron的类需要初始化)

import com.gamer.um.quartz.configure.SchedulerJob;
import com.gamer.um.quartz.jobs.MonitorCronJob;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

/**
 * @version v1.0
 * @description:
 * @author: 47 on 2020/4/9 17:12
 */
@Component
public class Initialization implements ApplicationRunner {

    @Autowired
    private SchedulerManager schedulerManager;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        SchedulerJob schedulerJob = new SchedulerJob();
        schedulerJob.setName(MonitorCronJob.JOB_NAME);
        schedulerJob.setGroup(MonitorCronJob.GROUP_NAME);
        schedulerJob.setCron(MonitorCronJob.CRON);
        schedulerJob.setDesc(MonitorCronJob.DESC);
        schedulerJob.setJobClass(MonitorCronJob.class.getName());
        schedulerManager.activeJob(schedulerJob);
    }
}

8.测试

public class MyJob implements Job {

  @Autowired
  private SchedulerManager schedulerManager;

 测试时需要去过滤下当前Jobs里的job是当前执行的(下面会给出代码)
  .......你的业务代码    
}

@Override
public SchedulerJob getSchedulerJobDetail(Class jobClass) {
  List<SchedulerJob> jobs = schedulerJobs.getJobs();
  if (Objects.isNull(jobs)) {
    return null;
  }
  SchedulerJob job = jobs.stream()
    .filter(schedulerJob -> Objects.equals(schedulerJob.getJobClass(), jobClass.getName()))
    .findFirst()
    .orElse(null);
  return job;
}

posted @ 2020-09-29 23:41  47号Gamer丶  阅读(3000)  评论(0编辑  收藏  举报