定时任务能力进击!Quartz框架的使用

233 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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集成

  1. 构建maven依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
    
  2. 建立数据库表,其实不新增数据库表也可以集成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 张表

  3. 项目增加配置文件

    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=10
    

    2) bootstrap.properties

     spring.quartz.jdbc.initialize-schema: always
     spring.quartz.job-store-type: jdbc
     ```
    
  4. 增加配置类

     @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;
         }
     }
    
     
    
  5. 任务管理类

    新增定时任务管理类,可以对定时任务进行增加和删除

    • 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);
            }
        }
    
    }
    
  6. 调用

     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的功能能力,所以说技术的迭代都是一点一点积累的,有了基础,学习新的技术也就简单很多了。