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;
}