springboot整合quartz项目使用(含完整代码)
Posted 小lee学编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot整合quartz项目使用(含完整代码)相关的知识,希望对你有一定的参考价值。
前言:quartz是一个定时调度的框架,就目前市场上来说,其实有比quartz更优秀的一些定时调度框架,不但性能比quartz好,学习成本更低,而且还提供可视化操作定时任务。例如xxl-Job,elastic-Job这两个算是目前工作中使用比较多的定时调度框架了,适配于分布式的项目,性能也是很优秀。这是很多人就很疑惑,既然这样我们为什么还要了解学习quartz呢?我个人觉得学习quartz有两方面,首先xxl-Job,elastic-Job这些框架都是基于quartz的基础上二次开发的,学习quartz更有利于我们加强理解定时调度。第二方面就是工作需求,有一些传统互联网公司还是有很多项目是使用quartz来完成定时任务的开发的,不懂quartz的话,老板叫你写个定时任务都搞不定。
1. quartz的基础概念
有上图可以看到,一个job可以给多个jobDetail封装,一个jobDetail可以给trigger来配置规则,但是一个trigger只能装配一个jobDetail。
scheduler:可以理解为定时任务的工作容器或者说是工作场所,所有定时任务都是放在里面工作,可以开启和停止。
trigger:可以理解为是定时任务任务的工作规则配置,例如说,没个几分钟调用一次,或者说指定每天那个时间点执行。
jobDetail:定时任务的信息,例如配置定时任务的名字,群组之类的。
job:定时任务的真正的业务处理逻辑的地方。
2. quartz的简单使用
这是quartz的api使用,在官网直接提供使用例子,但是在工作中用不到这种方式的
地址:https://www.quartz-scheduler.org/documentation/quartz-2.3.0/quick-start.html
public class QuartzTest
public static void main(String[] args) throws Exception
try
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
JobDetail job = newJob(HelloJob.class)
.withIdentity("job1", "group1")
.build();
Trigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();
scheduler.scheduleJob(job, trigger);
TimeUnit.SECONDS.sleep(20);
scheduler.shutdown();
catch (SchedulerException se)
se.printStackTrace();
3. quartz与springboot的整合使用
在官网中介绍了,只要你引用了quartz的依赖后,springboot会自适配调度器。当然我们也可以新建bean,修改SchedulerFactoryBean的一些默认属性值。
使用javaBean方式按实际业务需求初始化SchedulerFactoryBean(可以不要,就用默认SchedulerFactoryBean
@Configuration
public class QuartzConfiguration
// Quartz配置文件路径
private static final String QUARTZ_CONFIG = "config/quartz.properties";
@Value("$task.enabled:true")
private boolean enabled;
@Autowired
private DataSource dataSource;
@Bean
public SchedulerFactoryBean schedulerFactoryBean()
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setDataSource(dataSource);
// 设置加载的配置文件
schedulerFactoryBean.setConfigLocation(new ClassPathResource(QUARTZ_CONFIG));
// 用于quartz集群,QuartzScheduler 启动时更新己存在的Job
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setStartupDelay(5);// 系统启动后,延迟5s后启动定时任务,默认为0
// 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
schedulerFactoryBean.setOverwriteExistingJobs(true);
// SchedulerFactoryBean在初始化后是否马上启动Scheduler,默认为true。如果设置为false,需要手工启动Scheduler
schedulerFactoryBean.setAutoStartup(enabled);
return schedulerFactoryBean;
要使用quartz实现定时任务,首先要新建一个Job,在springboot中,新建的Job类要继承QuartzJobBean
public class HelloJob extends QuartzJobBean
@Override
protected void executeInternal(JobExecutionContext context)
StringJoiner joiner = new StringJoiner(" | ")
.add("---HelloJob---")
.add(context.getTrigger().getKey().getName())
.add(DateUtil.formatDate(new Date()));
System.out.println(joiner);
创建jobDetail和Trigger来启动定时任务,有两种方式可以实现,本质上就是创建jobDetail和Trigger
方式一:为对应的Job创建JobDetail和Trigger,这种方式有两个注意的地方,jobDetail一定要设置为可持久化.storeDurably(),Trigger创建要用.forJob(“helloJob”),要与JobDetail定义的相同。
@Component
public class HelloJobDetailConfig
@Bean
public JobDetail helloJobDetail()
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
.withIdentity("helloJob")
.storeDurably()
.usingJobData("data", "保密信息")
.build();
return jobDetail;
@Bean
public Trigger helloJobTrigger()
Trigger trigger = TriggerBuilder.newTrigger()
.forJob("helloJob")
.withSchedule(simpleSchedule()
.withIntervalInSeconds(3)
.repeatForever())
.build();
return trigger;
方式二:在注入Bean之前初始化创建JobDetail和Trigger,然后使用Scheduler来调用,跟原生API调用差不多。
@Component
public class HelloJobDetailConfig2
@Autowired
private Scheduler scheduler;
@PostConstruct
protected void InitHelloJob() throws Exception
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
.withIdentity("helloJob")
// .storeDurably()
.usingJobData("data", "保密信息")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("helloTrigger")
.withSchedule(simpleSchedule()
.withIntervalInSeconds(3)
.repeatForever())
.build();
scheduler.scheduleJob(jobDetail,trigger);
4. quartz的持久化
quartz持久化有两种存储,一般情况下quartz相关的表和业务表是放在同一个数据库里的。但是如果考虑性能问题的话,就要配多数据源,业务表单独一个库,quartz相关的表放在一个库。
https://docs.spring.io/spring-boot/docs/2.3.12.RELEASE/reference/html/spring-boot-features.html#boot-features-quartz
spring官网说明,默认情况下,使用内存中的JobStore。但是,如果应用程序中有DataSourcebean,并且spring.quartz是可用的,则可以配置基于JDBC的存储。将相应地配置作业存储类型属性。第二个配置,每次启动先删除表数据再重新创建(在实际生产中,个人更倾向于拿dml来手动创建表,这个值设置为never)。在quartz的jar包里这个路径下有不同数据库的dml:org.quartz.impl.jdbcjobstore
spring.quartz.job-store-type=jdbc
spring.quartz.jdbc.initialize-schema=never
另外一种方式:
要让Quartz使用DataSource而不是应用程序的主DataSource,请声明DataSourcebean,并用@QuartzDataSource注释其@bean方法。这样做可以确保SchedulerFactoryBean和模式初始化都使用Quartz特定的DataSource
@Configuration
public class QuartzDataSourceConfig
@Bean
@QuartzDataSource
public DataSource quartzDataSource()
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&useSSL=false");
return dataSource;
还有一点需要注意:当jobbean已经注入spring容器后,下次不用需要再注入,把@Component注释掉。
5. quartz的misfire策略
**misfire:**到了任务触发时间点,但是任务没有被触发
原因:- 使用@DisallowConcurrentExecution注解,而且任务的执行时间>任务间隔
-线程池满了,没有资源执行任务
-机器宕机或者认为停止,果断时间恢复运行。
@DisallowConcurrentExecution:这个是比较常用的注解,证上一个任务执行完后,再去执行下一个任务,不会允许任务并行执行。
@PersistJobDataAfterExecution:任务执行完后,会持久化保留数据到下次 执行
针对不同的ScheduleBuilder,可以设置不同的失火策略,SimpleScheduleBuilder和非SimpleScheduleBuilder,
SimpleScheduleBuilder有六种,而非SimpleScheduleBuilder有三种,在实际工作中我们使用的比较的是CronScheduleBuilder.
.withMisfireHandlingInstructionIgnoreMisfires()
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1
所有未触发的执行都会立即执行,然后触发器再按计划运行。
.withMisfireHandlingInstructionFireAndProceed()
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW = 1
立即执行第一个错误的执行并丢弃其他(即所有错误的执行合并在一起),也就是说无论错过了多少次触发器的执行,都只会立即执行一次,然后触发器再按计划运行。(默认的失火策略)
.withMisfireHandlingInstructionDoNothing()
MISFIRE_INSTRUCTION_DO_NOTHING = 2
所有未触发的执行都将被丢弃,然后再触发器的下一个调度周期按计划运行。
6、总结
关于quartz还有一个很重要的点就是corn表达式,这个个人认为没必要死记硬背,实在不会写的,上网找corn表达式在线转换就可以了。
一个简单demo的代码地址:https://gitee.com/gorylee/quartz-demo
生产项目中quartz的配置使用代码地址:https://gitee.com/gorylee/learnDemo/tree/master/quartzDemo
springboot-01整合quartz
1、什么是quartz?
quartz是一个开源的定时任务框架,具备将定时任务持久化至数据库以及分布式环境下多节点调度的能力。当当的elastic-job便是以quartz为基础,结合zookeeper开发出来的一款产品。
2、整合springboot示例
2.1)引入quartz依赖
<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>
2.2)配置quartz
1 /** 2 * 分布式定时任务quartz配置 3 * Created by chenjunyi on 2018/6/6. 4 */ 5 @Configuration 6 public class QuartzConfiguration { 7 8 /** 9 * quartz的JobFactory,根据注册的JobClass从spring应用上下文中获取job实例 10 */ 11 public static class AutoSpringBeanJobFactory extends AdaptableJobFactory implements SchedulerContextAware { 12 13 /** spring应用上下文 */ 14 private ApplicationContext applicationContext; 15 16 /** scheduler上下文 */ 17 private SchedulerContext schedulerContext; 18 19 /** 需要忽略的属性 */ 20 private String[] ignoredUnknownProperties = null; 21 22 private void setApplicationContext(ApplicationContext applicationContext) { 23 this.applicationContext = applicationContext; 24 } 25 26 @Override 27 public void setSchedulerContext(SchedulerContext schedulerContext) { 28 this.schedulerContext = schedulerContext; 29 } 30 31 private void setIgnoredUnknownProperties(String... ignoredUnknownProperties) { 32 this.ignoredUnknownProperties = ignoredUnknownProperties; 33 } 34 35 @Override 36 protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception { 37 //获取定时任务的clazz,并从spring上下文中获取实例 38 Class<? extends Job> clazz = bundle.getJobDetail().getJobClass(); 39 Job job = applicationContext.getBean(clazz); 40 41 if (isEligibleForPropertyPopulation(job)) { 42 //非继承自QuartzJobBean的Job,设置job属性 43 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job); 44 MutablePropertyValues pvs = new MutablePropertyValues(); 45 if (this.schedulerContext != null) { 46 pvs.addPropertyValues(this.schedulerContext); 47 } 48 pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap()); 49 pvs.addPropertyValues(bundle.getTrigger().getJobDataMap()); 50 if (this.ignoredUnknownProperties != null) { 51 for (String propName : this.ignoredUnknownProperties) { 52 if (pvs.contains(propName) && !bw.isWritableProperty(propName)) { 53 pvs.removePropertyValue(propName); 54 } 55 } 56 bw.setPropertyValues(pvs); 57 } else { 58 bw.setPropertyValues(pvs, true); 59 } 60 } 61 return job; 62 } 63 64 private boolean isEligibleForPropertyPopulation(Object jobObject) { 65 return (!(jobObject instanceof QuartzJobBean)); 66 } 67 68 } 69 70 /** 71 * 配置任务工厂实例 72 * @param applicationContext spring上下文实例 73 * @return 任务工厂实例 74 */ 75 @Bean 76 public JobFactory jobFactory(ApplicationContext applicationContext) { 77 AutoSpringBeanJobFactory jobFactory = new AutoSpringBeanJobFactory(); 78 jobFactory.setApplicationContext(applicationContext); 79 return jobFactory; 80 } 81 82 /** 83 * 配置任务调度器,使用项目数据源作为quartz数据源 84 * @param jobFactory 自定义配置任务工厂 85 * @param dataSource 数据源实例 86 * @return 任务调度器 87 */ 88 @Bean 89 public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) { 90 SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); 91 //将spring管理job自定义工厂交由调度器维护 92 schedulerFactoryBean.setJobFactory(jobFactory); 93 //设置覆盖已存在的任务(配置已失效,因为改写了原有的注册方式,JOB注册时便已自动进行替换) 94 schedulerFactoryBean.setOverwriteExistingJobs(true); 95 //项目启动完成后,等待50秒后开始执行调度器初始化(需要小于JOB的间隔时间) 96 schedulerFactoryBean.setStartupDelay(50); 97 //设置调度器自动运行 98 schedulerFactoryBean.setAutoStartup(true); 99 //设置数据源,使用与项目统一数据源 100 schedulerFactoryBean.setDataSource(dataSource); 101 //设置定时调度器命名空间 102 schedulerFactoryBean.setSchedulerName("MY-QUARTZ-SCHEDULER"); 103 //设置存储在quartz上文中的Spring应用上下文key 104 schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext"); 105 //设置属性 106 Properties properties = new Properties(); 107 //设置调度器实例名 108 properties.setProperty("org.quartz.scheduler.instanceName", "SCHEDULER-INSTANCE"); 109 //设置调度器实例ID,在cluster中使用,AUTO标识自动生成 110 properties.setProperty("org.quartz.scheduler.instanceId", "AUTO"); 111 //禁用rmi配置 112 properties.setProperty("org.quartz.scheduler.rmi.export", "false"); 113 //禁用rmi配置 114 properties.setProperty("org.quartz.scheduler.rmi.proxy", "false"); 115 //quartz线程池实现类 116 properties.setProperty("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); 117 //quartz线程池线程数 118 properties.setProperty("org.quartz.threadPool.threadCount", "10"); 119 //quartz线程池线程优先级 120 properties.setProperty("org.quartz.threadPool.threadPriority", "5"); 121 //quartz线程池是否自动加载数据库内的定时任务 122 properties.setProperty("org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread", "true"); 123 //Job错过执行时间的阈值 124 properties.setProperty("org.quartz.jobStore.misfireThreshold", "60000"); 125 //Job持久化方式配置 126 properties.setProperty("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX"); 127 //Job的JDBC持久化驱动,此处配置为MySql 128 properties.setProperty("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate"); 129 //配置是否使用 130 properties.setProperty("org.quartz.jobStore.useProperties", "false"); 131 //持久化的quartz表结构前缀 132 properties.setProperty("org.quartz.jobStore.tablePrefix", "QRTZ_"); 133 //是否是集群quartz 134 properties.setProperty("org.quartz.jobStore.isClustered", "true"); 135 //集群quartz中节点有效性检查时间间隔 136 properties.setProperty("org.quartz.jobStore.clusterCheckinInterval", "20000"); 137 //错过执行时间的Job最大持有数 138 properties.setProperty("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1"); 139 schedulerFactoryBean.setQuartzProperties(properties); 140 //返回结果 141 return schedulerFactoryBean; 142 } 143 144 }
2.3)编写定时任务
1 /** 2 * 抽象定时任务,完成向quartz注册的功能 3 * Created by chenjunyi on 2018/6/6. 4 */ 5 @Slf4j 6 public abstract class AbstractScheduler implements InterruptableJob { 7 8 @Autowired 9 private Scheduler scheduler; 10 11 /** 12 * 向定时任务调度器注册 13 * @throws SchedulerException 注册时发生异常 14 */ 15 @PostConstruct 16 protected void register() throws SchedulerException { 17 //任务和触发器名称(若不进行设置,则quartz默认使用UUID,因此每次启动应用都会注册一个新任务) 18 String jobName = this.getClass().getSimpleName() + "Job"; 19 String triggerName = this.getClass().getSimpleName() + "Trigger"; 20 //设置定时任务 21 JobDetail jobDetail = JobBuilder.newJob(this.getClass()).withIdentity(jobName).build(); 22 //创建任务触发器 23 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(getCron()); 24 Trigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerName).withSchedule(scheduleBuilder).build(); 25 //将触发器与任务绑定到调度器内 26 Set<Trigger> triggers = new HashSet<>(); 27 triggers.add(trigger); 28 scheduler.scheduleJob(jobDetail, triggers, true); 29 log.info(">>>>>注册[Trigger={}, Job={}]的定时任务成功", triggerName, jobName); 30 } 31 32 /** 33 * 获取cron表达式 34 * @return cron表达式 35 */ 36 protected abstract String getCron(); 37 38 }
实现一个自定义的demo定时任务,并通过@Service交于Spring-IOC容器进行托管,代码如下:
1 /** 2 * 示例定时任务 3 * Created by chenjunyi on 2018/6/5. 4 */ 5 @Service 6 @PersistJobDataAfterExecution 7 @DisallowConcurrentExecution 8 public class DemoScheduler extends AbstractScheduler { 9 10 @Value("${env.cron.demoScheduler}") 11 private String cron; 12 13 @Autowired 14 private DemoService demoService; 15 16 @Override 17 public void interrupt() throws UnableToInterruptJobException { 18 19 } 20 21 @Override 22 public void execute(JobExecutionContext context) throws JobExecutionException { 23 demoService.sayHello(); 24 } 25 26 @Override 27 protected String getCron() { 28 return this.cron; 29 } 30 31 }
3、整合代码详解
3.1)AutoSpringBeanJobFactory
1 @Override 2 public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException { 3 try { 4 Object jobObject = createJobInstance(bundle); //直接根据class.newInstance获取实例 5 return adaptJob(jobObject); //job适配,判断job类型并进行包装,此处忽略 6 } catch (Exception ex) { 7 throw new SchedulerException("Job instantiation failed", ex); 8 } 9 } 10 11 protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { 12 return bundle.getJobDetail().getJobClass().newInstance(); 13 }
1 //参考博客链接:https://www.jianshu.com/p/d52d62fb2ac6;作者:恒宇少年 2 public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware { 3 4 /** spring-beanfactory */ 5 private transient AutowireCapableBeanFactory beanFactory; 6 7 @Override 8 public void setApplicationContext(final ApplicationContext context) { 9 beanFactory = context.getAutowireCapableBeanFactory(); 10 } 11 12 @Override 13 protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception { 14 final Object job = super.createJobInstance(bundle); 15 beanFactory.autowireBean(job); //通过newInstance获取job实例后,将其交付给spring-ioc 16 return job; 17 } 18 }
1 Class<? extends Job> clazz = bundle.getJobDetail().getJobClass(); 2 Job job = applicationContext.getBean(clazz);
3.2)AbstractScheduler
- 需要设置任务和触发器名称。如果不设置,quartz默认使用UUID,因此每次启动应用都会注册一个新任务;
- 调度器的绑定,要使用含有replace功能的方法(方法参数带有boolean replace的)。由于是将Job持久化到数据库,应用再次启动时,会读取数据库中的任务列表。不含replace功能的方法在进行一次新的注册时,发现任务已存在的话,就会报错;而含replace功能的方法会更新数据库的Job配置信息;
- 由于我们在Job初始化时便进行了任务注册,且采用的是replace的方式,因此在config中的schedulerFactoryBean.setOverwriteExistingJobs(true)该条配置便失效了(因为SchedulerFactoryBean在afterPropertiesSet这个属于SpringBean的生命周期方法中,调用了自身的registerJobsAndTriggers方法,该方法会根据此参数决定是否调用含replace功能的绑定方法进行更新Job,我们自己的Job注册实现中便完成了此功能);
- 继续上一条,值得注意的是,SchedulerFactoryBean.registerJobsAndTriggers的方法中,会根据是否设置了TransactionManager来决定是否将所有Job和Trigger的更新放在同一个事务中,由于目前的应用没有需要使用事务来更新Job的需求,并且若更新失败,启动应用时会抛出异常,因此该问题放置待解决(解决办法也很简单,在register方法中添加事务控制即可);
以上是关于springboot整合quartz项目使用(含完整代码)的主要内容,如果未能解决你的问题,请参考以下文章
SpringBoot 整合Quartz定时任务管理SpringBoot系列18