动态更改Spring定时任务Cron表达式的优雅方案

0x01 前言

在 SpringBoot 项目中,我们可以通过@EnableScheduling注解开启调度任务支持,并通过@Scheduled注解快速地建立一系列定时任务。

@Scheduled支持下面三种配置执行时间的方式:

  • cron(expression):根据Cron表达式来执行。

  • fixedDelay(period):固定间隔时间执行,无论任务执行长短,两次任务执行的间隔总是相同的。

  • fixedRate(period):固定频率执行,从任务启动之后,总是在固定的时刻执行,如果因为执行时间过长,造成错过某个时刻的执行(晚点),则任务会被立刻执行。

最常用的应该是第一种方式,基于Cron表达式的执行模式,因其相对来说更加灵活。

0x02 可变与不可变

默认情况下,@Scheduled注解标记的定时任务方法在初始化之后,是不会再发生变化的。Spring 在初始化 bean 后,通过后处理器拦截所有带有@Scheduled注解的方法,并解析相应的的注解参数,放入相应的定时任务列表等待后续统一执行处理。到定时任务真正启动之前,我们都有机会更改任务的执行周期等参数。换言之,我们既可以通过application.properties配置文件配合@Value注解的方式指定任务的Cron表达式,亦可以通过CronTrigger从数据库或者其他任意存储中间件中加载并注册定时任务。这是 Spring 提供给我们的可变的部分。

但是我们往往要得更多。能否在定时任务已经在执行过的情况下,去动态更改Cron表达式,甚至禁用某个定时任务呢?很遗憾,默认情况下,这是做不到的,任务一旦被注册和执行,用于注册的参数便被固定下来,这是不可变的部分。

0x03 创造与毁灭

既然创造之后不可变,那就毁灭之后再重建吧。于是乎,我们的思路便是,在注册期间保留任务的关键信息,并通过另一个定时任务检查配置是否发生变化,如果有变化,就把“前任”干掉,取而代之。如果没有变化,就保持原样。

先对任务做个简单的抽象,方便统一的识别和管理:

publicinterfaceIPollableService{/**
     * 执行方法
     */voidpoll();/**
     * 获取周期表达式
     *
     * @return CronExpression
     */defaultStringgetCronExpression(){returnnull;}/**
     * 获取任务名称
     *
     * @return 任务名称
     */defaultStringgetTaskName(){returnthis.getClass().getSimpleName();}}

最重要的便是getCronExpression()方法,每个定时服务实现可以自己控制自己的表达式,变与不变,自己说了算。至于从何处获取,怎么获取,请诸君自行发挥了。接下来,就是实现任务的动态注册:

@Configuration@EnableAsync@EnableSchedulingpublicclassSchedulingConfigurationimplementsSchedulingConfigurer,ApplicationContextAware{privatestaticfinalLogger log =LoggerFactory.getLogger(SchedulingConfiguration.class);privatestaticApplicationContext appCtx;privatefinalConcurrentMap<String,ScheduledTask> scheduledTaskHolder =newConcurrentHashMap<>(16);privatefinalConcurrentMap<String,String> cronExpressionHolder =newConcurrentHashMap<>(16);privateScheduledTaskRegistrar taskRegistrar;publicstaticsynchronizedvoidsetAppCtx(ApplicationContext appCtx){SchedulingConfiguration.appCtx = appCtx;}@OverridepublicvoidsetApplicationContext(ApplicationContext applicationContext)throwsBeansException{setAppCtx(applicationContext);}@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrar taskRegistrar){this.taskRegistrar = taskRegistrar;}/**
     * 刷新定时任务表达式
     */publicvoidrefresh(){Map<String,IPollableService> beanMap = appCtx.getBeansOfType(IPollableService.class);if(beanMap.isEmpty()|| taskRegistrar ==null){return;}
        beanMap.forEach((beanName, task)->{String expression = task.getCronExpression();String taskName = task.getTaskName();if(null== expression){
                log.warn("定时任务[{}]的任务表达式未配置或配置错误,请检查配置", taskName);return;}// 如果策略执行时间发生了变化,则取消当前策略的任务,并重新注册任务boolean unmodified = scheduledTaskHolder.containsKey(beanName)&& cronExpressionHolder.get(beanName).equals(expression);if(unmodified){
                log.info("定时任务[{}]的任务表达式未发生变化,无需刷新", taskName);return;}Optional.ofNullable(scheduledTaskHolder.remove(beanName)).ifPresent(existTask ->{
                existTask.cancel();
                cronExpressionHolder.remove(beanName);});if(ScheduledTaskRegistrar.CRON_DISABLED.equals(expression)){
                log.warn("定时任务[{}]的任务表达式配置为禁用,将被不会被调度执行", taskName);return;}CronTask cronTask =newCronTask(task::poll, expression);ScheduledTask scheduledTask = taskRegistrar.scheduleCronTask(cronTask);if(scheduledTask !=null){
                log.info("定时任务[{}]已加载,当前任务表达式为[{}]", taskName, expression);
                scheduledTaskHolder.put(beanName, scheduledTask);
                cronExpressionHolder.put(beanName, expression);}});}}

重点是保存ScheduledTask对象的引用,它是控制任务启停的关键。而表达式“-”则作为一个特殊的标记,用于禁用某个定时任务。当然,禁用后的任务通过重新赋予新的 Cron 表达式,是可以“复活”的。完成了上面这些,我们还需要一个定时任务来动态监控和刷新定时任务配置:

@ComponentpublicclassCronTaskLoaderimplementsApplicationRunner{privatestaticfinalLogger log =LoggerFactory.getLogger(CronTaskLoader.class);privatefinalSchedulingConfiguration schedulingConfiguration;privatefinalAtomicBoolean appStarted =newAtomicBoolean(false);privatefinalAtomicBoolean initializing =newAtomicBoolean(false);publicCronTaskLoader(SchedulingConfiguration schedulingConfiguration){this.schedulingConfiguration = schedulingConfiguration;}/**
     * 定时任务配置刷新
     */@Scheduled(fixedDelay =5000)publicvoidcronTaskConfigRefresh(){if(appStarted.get()&& initializing.compareAndSet(false,true)){
            log.info("定时调度任务动态加载开始>>>>>>");try{
                schedulingConfiguration.refresh();}finally{
                initializing.set(false);}
            log.info("定时调度任务动态加载结束<<<<<<");}}@Overridepublicvoidrun(ApplicationArguments args){if(appStarted.compareAndSet(false,true)){cronTaskConfigRefresh();}}}

当然,也可以把这部分代码直接整合到SchedulingConfiguration中,但是为了方便扩展,这里还是将执行与触发分离了。毕竟除了通过定时任务触发刷新,还可以在界面上通过按钮手动触发刷新,或者通过消息机制回调刷新。这一部分就请大家根据实际业务情况来自由发挥了。

0x04 验证

我们创建一个原型工程和三个简单的定时任务来验证下,第一个任务是执行周期固定的任务,假设它的Cron表达式永远不会发生变化,像这样:

@ServicepublicclassCronTaskBarimplementsIPollableService{@Overridepublicvoidpoll(){System.out.println("Say Bar");}@OverridepublicStringgetCronExpression(){return"0/1 * * * * ?";}}

第二个任务是一个经常更换执行周期的任务,我们用一个随机数发生器来模拟它的善变:

@ServicepublicclassCronTaskFooimplementsIPollableService{privatestaticfinalRandom random =newSecureRandom();@Overridepublicvoidpoll(){System.out.println("Say Foo");}@OverridepublicStringgetCronExpression(){return"0/"+(random.nextInt(9)+1)+" * * * * ?";}}

第三个任务就厉害了,它仿佛就像一个电灯的开关,在启用和禁用中反复横跳:

@ServicepublicclassCronTaskUnavailableimplementsIPollableService{privateString cronExpression ="-";privatestaticfinalMap<String,String> map =newHashMap<>();static{
        map.put("-","0/1 * * * * ?");
        map.put("0/1 * * * * ?","-");}@Overridepublicvoidpoll(){System.out.println("Say Unavailable");}@OverridepublicStringgetCronExpression(){return(cronExpression = map.get(cronExpression));}}

如果上面的步骤都做对了,日志里应该能看到类似这样的输出:

定时调度任务动态加载开始>>>>>>
定时任务[CronTaskBar]的任务表达式未发生变化,无需刷新
定时任务[CronTaskFoo]已加载,当前任务表达式为[0/6 * * * * ?]
定时任务[CronTaskUnavailable]的任务表达式配置为禁用,将被不会被调度执行
定时调度任务动态加载结束<<<<<<
Say Bar
Say Bar
Say Foo
Say Bar
Say Bar
Say Bar
定时调度任务动态加载开始>>>>>>
定时任务[CronTaskBar]的任务表达式未发生变化,无需刷新
定时任务[CronTaskFoo]已加载,当前任务表达式为[0/3 * * * * ?]
定时任务[CronTaskUnavailable]已加载,当前任务表达式为[0/1 * * * * ?]
定时调度任务动态加载结束<<<<<<
Say Unavailable
Say Bar
Say Unavailable
Say Bar
Say Foo
Say Unavailable
Say Bar
Say Unavailable
Say Bar
Say Unavailable
Say Bar

0x05 小结

我们在上文通过定时刷新和重建任务的方式来实现了动态更改Cron表达式的需求,能够满足大部分的项目场景,而且没有引入quartzs等额外的中间件,可以说是十分的轻量和优雅了。当然,如果各位看官有更好的方法,还请不吝赐教。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
【项目介绍】 基于Java开发的动态定时任务管理系统源码+使用说明.zip 启动 1. csdn下载该项目源码。 2. 本地数据库创建一个名为 `scheduling` 的库。 3. 修改配置文件 `src/main/resources/application.yml`,主要修改数据库连接的用户名和地址。 4. 启动项目。 5. 浏览器访问 `http://localhost:8080`,可以看到如下页面: 功能介绍 1. 项目启动时,会自动从数据库中加载状态为 1 的定时任务并开始执行,1 表示处于开启状态的定时任务,0 表示处于禁用状态的定时任务。 2. 点击页面上的**添加作业**按钮,可以添加一个新的定时任务,新任务的 Bean 名称、方法名称以及方法参数如果和已有的记录相同,则认为是重复作业,重复作业会添加失败。 这里涉及到几个参数,含义如下: - Bean 名称:这是项目中注入 Spring 的 Bean 名称,测试代码中以 `SchedulingTaskDemo.java` 为例。 - 方法名称:参数 1 中 bean 里边的方法名称。 - 方法参数:参数 2 中方法的参数。 - Cron 表达式定时任务Cron 表达式。 - 作业状态:开启和禁用两种。开启的话,添加完成后这个定时任务就会开始执行,禁用的话,就单纯只是将记录添加到数据库中。 3. 点击作业编辑,可以修改作业的各项数据: 修改后会立马生效。 4. 点击作业删除,可以删除一个现有的作业。假如删除的作业正在执行,则先停止该作业,然后删除。 5. 点击列表中的 switch 按钮也可以切换作业的状态。 ## 技术栈 - SpringBoot - Jpa - MySQL - Spring Job - Vue 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通。 2、项目适用人群:计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等计算机相关专业的在校学生、专业老师、行业内企业员工下载使用。 3、项目可用于:项目本身具有较高的学习借鉴价值,不仅适用于小白学习进阶,也可用于专业人员二次开发。当然也可作为毕设项目、课程设计、课程大作业、初期项目立项演示等。 4、如果基础还行,或者热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载使用,相互学习,共同进步!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

盈梓的博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值