持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
一 前言
项目中要使用定时任务能力发送短信,项目是微服务部署的,之前所有的定时任务使用的是xxl-job,但是我们这次是动态新增定时任务,而xxl-job对于动态新增定时任务是不够友好的,所以就想选择其他的技术方案,前面我写过一篇文章写了对技术的选择以及实现,是通过schedule线程池来实现,自己去新增定时任务配置表,但是随着更多的接入方使用这个定时能力,考虑到功能健壮性,查阅一些资料,发现quartz这个定时任务框架十分适合项目。于是对项目进行了quartz框架的集成与使用。
二 quartz简介
Quartz 是 OpenSymphony 开源组织在 Job Scheduling 领域又一个开源项目,是完全由 Java 开发的一个开源任务日程管理系统,“任务进度管理器”就是一个在预先确定(被纳入日程)的时间到达时,负责执行(或者通知)其他软件组件的系统。 Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中,它提供了巨大的灵活性而不牺牲简单性
当定时任务愈加复杂时,使用 Spring 注解 @Schedule 已经不能满足业务需要
在项目开发中,经常需要定时任务来帮助我们来做一些内容,如定时派息、跑批对账、将任务纳入日程或者从日程中取消,开始,停止,暂停日程进度等。SpringBoot 中现在有两种方案可以选择,第一种是 SpringBoot 内置的方式简单注解就可以使用,当然如果需要更复杂的应用场景还是得 Quartz 上场,Quartz 目前是 Java 体系中最完善的定时方案
三 quartz集成
-
构建maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> -
建立数据库表,其实不新增数据库表也可以集成quartz,新增数据库表更健壮,不建立数据库表服务停了之前保存在内存中的定时任务就没有了 sql基本www.quartz-scheduler.org/downloads/,解压后进入如下目录可以找到sql文件:~\quartz-2.3.0-distribution\quartz-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore,名称为
tables_mysql.sql,创建成功后数据库中多出11张表 -
项目增加配置文件
1) quartz.properties
# 实例化ThreadPool时,使用的线程类为SimpleThreadPool org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool # threadCount和threadPriority将以setter的形式注入ThreadPool实例 # 并发个数 org.quartz.threadPool.threadCount=10 # 优先级 org.quartz.threadPool.threadPriority=5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true org.quartz.jobStore.misfireThreshold=5000 #持久化使用的类 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX #数据库中表的前缀 org.quartz.jobStore.tablePrefix=QRTZ_ #数据源命名 org.quartz.jobStore.dataSource=qzDS #qzDS 数据源,我们使用hikaricp,默认的是c3p0 org.quartz.dataSource.qzDS.provider=hikaricp org.quartz.dataSource.qzDS.driver=com.mysql.cj.jdbc.Driver org.quartz.dataSource.qzDS.URL=jdbc:mysql://127.0.0.1:3306/dst_db_message?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai org.quartz.dataSource.qzDS.user=xxx org.quartz.dataSource.qzDS.password=xxx org.quartz.dataSource.qzDS.maxConnections=102) bootstrap.properties
spring.quartz.jdbc.initialize-schema: always spring.quartz.job-store-type: jdbc ``` -
增加配置类
@Component public class JobFactory extends SpringBeanJobFactory { @Autowired private AutowireCapableBeanFactory beanFactory; /** * 这里覆盖了super的createJobInstance方法,对其创建出来的类再进行autowire */ @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { Object jobInstance = super.createJobInstance(bundle); beanFactory.autowireBean(jobInstance); return jobInstance; } } /** * @Description: 初始化 quartz配置 * @author: qianyun * @date: 2021/12/20 */ @Configuration public class QuartzConfig { @Autowired private JobFactory jobFactory; /** * 初始化配置文件 * @author qianyun * @date 2022/5/19 15:37 * @param : * @return Properties */ @Bean public Properties quartzProperties() throws IOException { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties")); propertiesFactoryBean.afterPropertiesSet(); return propertiesFactoryBean.getObject(); } @Bean public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); schedulerFactoryBean.setJobFactory(jobFactory); schedulerFactoryBean.setQuartzProperties(quartzProperties()); return schedulerFactoryBean; } /** * 初始化监听器 * * @return */ @Bean public QuartzInitializerListener executorListener() { return new QuartzInitializerListener(); } @Bean(name = "scheduler") public Scheduler scheduler() throws IOException { return schedulerFactoryBean().getScheduler(); } } /** * @Description:创建触发器 * @author: yjw * @date: 2021/12/20 */ public class TriggerComponent { /** * 构建cron触发器 * @author yangjiawen * @date 2022/5/19 15:40 * @param cron: * @return Trigger */ public static Trigger cronTrigger(String cron) { CronTrigger cronTrigger = TriggerBuilder.newTrigger() .withSchedule(CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing()) .build(); return cronTrigger; } public static Trigger cronTrigger(String cron, JobDataMap jobDataMap) { CronTrigger cronTrigger = TriggerBuilder.newTrigger() .withSchedule(CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing()) .usingJobData(jobDataMap) .build(); return cronTrigger; } } -
任务管理类
新增定时任务管理类,可以对定时任务进行增加和删除
- JobDetail类 主要职责是管理定时任务要执行的行为
- CronTrigger类 职责是什么时候执行定时任务
- Scheduler类 主要职责是开启定时任务
/** * @Description:quartz定时任务管理类 * @author: yjw * @date: 2021/12/20 */ @Component @Slf4j public class QuartzJobManager { private static final String JOB_DEFAULT_GROUP_NAME = "job_group_loan_em_"; private static final String TRIGGER_DEFAULT_GROUP_NAME = "trigger_group_loan_em_"; private static final String JOB_DEFAULT_NAME = "jobName_"; private static final String TRIGGER_DEFAULT_NAME = "trigger_"; @Autowired private Scheduler scheduler; /** * 添加定时任务 * * @param jobClass 需要执行的业务逻辑service类 * @param cronExp cron表达式 * @param dataMap 需要传递以及存储的数据 * @param jobName 全局唯一的job key名称 * @return */ public void addJob(Class<? extends Job> jobClass, String cronExp, Map<String, Object> dataMap, String jobName) { try { if (!CronExpression.isValidExpression(cronExp)) { log.error("invalid cron expression: {}", cronExp); throw new SchedulerException("invalid cron expression"); } JobDetail jobDetail = JobBuilder.newJob(jobClass) .withIdentity(new JobKey(StringUtils.join(JOB_DEFAULT_NAME, jobName), StringUtils.join(JOB_DEFAULT_GROUP_NAME, jobName))) .build(); if (dataMap != null && dataMap.size() > 0) { jobDetail.getJobDataMap().putAll(dataMap); } CronTrigger trigger = TriggerBuilder.newTrigger() .forJob(jobDetail) .withIdentity(new TriggerKey(StringUtils.join(TRIGGER_DEFAULT_NAME, jobName), StringUtils.join(TRIGGER_DEFAULT_GROUP_NAME, jobName))) .withSchedule(CronScheduleBuilder.cronSchedule(cronExp)) .build(); scheduler.scheduleJob(jobDetail, trigger); if (!scheduler.isShutdown()) { scheduler.start(); } } catch (Exception e) { log.error(e.getMessage()); throw new RuntimeException(e); } } /** * 删除定时任务 * * @param jobName 全局唯一的job key名称(与添加任务时传递的一致) * @return */ public void deleteJob(String jobName) { try { JobKey jobKey = new JobKey(StringUtils.join(JOB_DEFAULT_NAME, jobName), StringUtils.join(JOB_DEFAULT_GROUP_NAME, jobName)); if (!scheduler.checkExists(jobKey)) { log.info("job not exists, job name: {}, group name: {}", jobKey.getName(), jobKey.getGroup()); return; } TriggerKey triggerKey = TriggerKey.triggerKey(StringUtils.join(TRIGGER_DEFAULT_NAME, jobName), StringUtils.join(TRIGGER_DEFAULT_GROUP_NAME, jobName)); scheduler.pauseTrigger(triggerKey); scheduler.unscheduleJob(triggerKey); scheduler.deleteJob(jobKey); } catch (Exception e) { log.error(e.getMessage()); throw new RuntimeException(e); } } } -
调用
JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put(TASK_ID, messageSmsSendTask.getId()); quartzJobManager.addJob(SmsTimingTaskExecute.class, CronUtils.getCron(sendTime) , jobDataMap, messageSmsSendTask.getJobName()); @Slf4j public class SmsTimingTaskExecute extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context){ JobDataMap dataMap = context.getMergedJobDataMap(); Long taskId = dataMap.getLong("taskId"); System.out.println("任务id" + taskId); } ``` 输出任务id123456 这样quartz定时任务框架就集成完了,相对来说比较简单,比自己实现简化了很多
四 后记
quartz框架的使用使项目对动态定时任务更加简单了,也更健壮了,相对之前我的实现更重一些,毕竟新增了较多的数据库表,但是考虑到未来功能被广泛使用,还是使用更健壮的quartz框架更合适一些,除此之外,quartz框架也提供了很多其他的api,功能上更强大一些,xxl-job的底层就较多的引用了quartz的功能能力,所以说技术的迭代都是一点一点积累的,有了基础,学习新的技术也就简单很多了。