Quartz使用心得

1. 适用场景

当业务涉及到动态定时任务时,我首先能想到的就是Quartz。目前有很多种实现定时任务的方式,比如:

  • ScheduledThreadPoolExecutor
  • timer
  • spring自带的@Scheduled
  • quartz

使用方式不进行过多介绍,但是前三者在使用的过程中都不是那么灵活,往往服务启动后,定时任务便固定,想要进行不停机修改很不容易。而quartz完美解决动态定时任务的问题,它提供了多样的api,可以进行定时任务的暂停、恢复、定时时间的修改等,同时还支持分布式定时任务。

2. quartz为什么这么强?

quartz这么灵活的原因与他本身的设计理念是分不开的。
核心概念:

  • Job表示一个工作,要执行具体的内容。此接口只有一个方法,就是用来执行业务逻辑的。
void execute(JobExecutionContext context) 
  • JobDetail表示一个具体的可执行调度程序,job是这个可执行程序所需要执行的内容。同时JobDetail还包含了一些任务调度的吧方案和策略。
  • Trigger调度参数,能设置什么时候取执行定时任务。用来设置定时周期的。
  • Scheduler调度容器,JobDetail和trigger在scheduler中注册就可以被Scheduler容器调度了。

从quartz的核心组件我们可以看到, 任务,定时周期,任务调度这三者是独立分割开来了,只有当三种通过某种方式组合在一起的时候,定时任务才会被最终执行,这就意味着拥有无限可能。我是否可以卸载某个scheduler中的trigger重新注册一个新的trigger上进行调度?我是否可以将任务暂时停止不去执行??

3. quartz整合springboot

关于quartz的基本使用和api我这边就不多介绍了,可以自行查看官方文档都很详细。网上整合quartz的文章也有很多,这边主要是说一下在使用过程中的一些坑。

  1. quartz第三方包版本问题导致RAMJobStore总是加载失败,报错是 “no setter method of 'useProperties'。
    使用springboot1.5.9 quartz 2.2.3报错。 将quartz版本升级到2.3.0正常启动。
    具体整合方案:
    gradle:
compile('org.quartz-scheduler:quartz:2.3.0')
compile('org.quartz-scheduler:quartz-jobs:2.3.0')

相关配置:

@Configuration
public class SchedulerConfig {
    @Autowired
    private CustomJobFactory customJobFactory;

    @Bean(name = "properties")
    public Properties quartzProperties() throws IOException {
        //加载配置信息
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        //新建quartz.properties  放在resources目录下
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }

    @Bean(name = "schedulerFactory")
    public SchedulerFactory schedulerFactory(Properties properties) throws IOException, SchedulerException {
        //采用功能强大的StdScheduleFactory
        SchedulerFactory schedulerFactory = new StdSchedulerFactory(properties);
        return schedulerFactory;
    }

    /**
     * 通过SchedulerFactory获取Scheduler的实例
     */
    @Bean(name = "scheduler")
    public Scheduler scheduler(SchedulerFactory schedulerFactory) throws IOException, SchedulerException {
        Scheduler scheduler = schedulerFactory.getScheduler();
        //设置自定义的jobFactory
        scheduler.setJobFactory(customJobFactory);
        scheduler.start();
        return scheduler;
    }
}

自定义JobFactory

@Component
public class CustomJobFactory extends AdaptableJobFactory {
    /**
     * 通过spring AutowireCapableBeanFactory将job对象加入到spring容器,提供给spring管理
     */
    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    @Override
    protected  Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        //调用父类方法创建job对象, 这里创建的对象是通过反射创建,思考一下会引发什么?
        Object jobInstance = super.createJobInstance(bundle);
        //注入到spring  采用这种注入方式,再job类中无法使用构造函数注入,只能通过变量注入
        capableBeanFactory.autowireBean(jobInstance);
        //autowireMode=4表示先查找构造函数,如果有就使用构造函数注入,否则就根据类型注入
        //capableBeanFactory.autowire(jobInstance.getClass(), 4, true);
        return jobInstance;
    }
}

需要执行的job任务

//坑: 注意,这里一定不要多此一举加上@Component, 因为之前已经通过capableBeanFactory进行注入,如果加上这里会创建实例报错
@DisallowConcurrentExecution
public class Task implements Job {
    private final static Logger logger = LogUtils.getLogger(RuleUpdateTask.class);
    @Autowired
    private Service service;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        logger.info("定时任务开始执行”);
        //.......
        service.save(obj)
    }
}

初始化时使用调度器执行定时任务:

@Component
public class InitStartSchedule implements CommandLineRunner {
    private final Logger logger = LogUtils.getLogger(InitStartSchedule.class);
    @Autowired
    private Scheduler scheduler;

    @Override
    public void run(String... args) throws Exception {
        //增加job以及trigger
        JobDetail jobDetail = JobBuilder.newJob(RuleUpdateTask.class).withIdentity("jobName", "jobGroupName").build();
        //采用表达式调度构建器
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(ruleUpdate.getCron());
        //trigger的key名称要保持和对应的jobDetail一致
        CronTrigger cronTrigger  = TriggerBuilder.newTrigger().withIdentity("jobName", "jobGroupName").withSchedule(scheduleBuilder).startNow().build();
        //任务不存在时添加
        if (!scheduler.checkExists(jobDetail.getKey())) {
            try {
                scheduler.scheduleJob(jobDetail, cronTrigger);
            } catch (SchedulerException e) {
                logger.info("创建定时任务失败", e);
                throw new Exception("创建定时任务失败");
            }
        }
    }
}

工具类实现定时任务的动态修改:

/**
 * @author jiandan
 * @Create 2020-12-23
 * 任务调度工具类, 更新, 增加, 暂停, 开启
 */
@Component
public class SchedulerUtil {
    private static final Logger logger = LogUtils.getLogger(SchedulerUtil.class);
    @Autowired
    private Scheduler scheduler;
    /**
     * 添加定时任务
     * @param cronExpression
     * @throws Exception
     */
    public void addJob(String cronExpression, String jobName, String jobGroupName, Class jobClass) throws Exception{
        //构建Job信息
        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                .withIdentity(jobName, jobGroupName)
                .build();
        //cron表达式构建器
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
        CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName, jobGroupName)
                .withSchedule(scheduleBuilder)
                .startNow().build();
        if (!scheduler.checkExists(jobDetail.getKey())){
            try {
                scheduler.scheduleJob(jobDetail, cronTrigger);
            } catch (SchedulerException e) {
                logger.info("创建定时任务失败" + e);
                throw new Exception("创建定时任务失败");
            }
        }
    }

    /**
     * 暂定job
     * @throws Exception
     */
    public void jobPause(String jobName, String jobGroupName) throws Exception {
        scheduler.pauseJob(JobKey.jobKey(jobName, jobGroupName));
    }


    /**
     * 启动job
     */
    public void jobResume(String jobName, String jobGroupName) throws Exception {
        scheduler.resumeJob(JobKey.jobKey(jobName, jobGroupName));
    }


    /**
     * 更新定时任务的cron表达式
     */
    public void jobReschedule(String jobName, String jobGroupName, String cronExpression) throws Exception {
        try{
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            //使用新的cronExpression表达式重新构建trigger
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).startNow().build();
            // 按照新的trigger重新设置job执行
            scheduler.rescheduleJob(triggerKey, trigger);
        } catch (SchedulerException e) {
            logger.error("更新定时任务失败", e);
            throw new Exception("更新定时任务失败");
        }
    }
}

quartz.properties, 配置文件根据自己所需进行填写,官网上有许多对于配置文件属性的讲解

#SimpleThreadPool维护一个固定的线程集,不会增长也不会缩小,功能和性能比较强大
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
#5个线程已经比较充足了
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
org.quartz.jobStore.misfireThreshold = 60000

#RAMJobStore性能最高,但是所有的数据保存在RAM,程序重启不会保留之前的调度数据
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

关于job中带一些业务数据并持久化保存可以参考quartz的持久化配置以及JobDataMap的api

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352