• SpringBoot系列之动态生成cron表达式执行定时程序


    业务场景

    最近需要实现一个功能,根据页面选择的星期,默认是凌晨执行,生成cron表达式,然后定时执行定时程序

    在这里插入图片描述

    环境准备

    • 开发环境

      • JDK 1.8
      • SpringBoot2.2.1
      • Maven 3.2+
    • 开发工具

      • IntelliJ IDEA
      • smartGit
      • Navicat15

    在IDEA里集成阿里的https://start.aliyun.com,创建一个Spring Initializr项目:
    在这里插入图片描述
    选择jdk版本,和maven打包方式,选择需要的dependencies
    在这里插入图片描述

    实现方案

    可以分两步实现:

    1. 先根据选择的星期生成cron表达式,保存到数据库里
    2. 根据保存的cron表达式规则执行定时程序

    生成cron表达式的可以写一些工具类,网上教程比较多,可以参考网上教程:Java生成cron表达式工具类

    生成cron表达式之后,保存到数据库里即可
    在这里插入图片描述
    有了动态配置的cron表达之后,就可以实现定时程序了,可以根据模板方法设计模式,写一个抽象的类,封装一些通用的方法,给子类实现业务

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.scheduling.annotation.SchedulingConfigurer;
    import org.springframework.scheduling.config.ScheduledTaskRegistrar;
    import org.springframework.scheduling.support.CronTrigger;
    
    @Slf4j
    public abstract class AbstractScheduler implements SchedulingConfigurer {
    
        @Override
        public void configureTasks(ScheduledTaskRegistrar registrar) {
            registrar.addTriggerTask(()->{
                // 执行业务
                doBusiness();
            }, triggerContext ->{
                String cron = this.getCronString();
                CronTrigger trigger;
                try {
                    trigger = new CronTrigger(cron);
                    return trigger.nextExecutionTime(triggerContext);
                }catch (Exception e) {
                    log.error("cron表达式异常,已经启用默认配置");
                    // 配置cron表达式异常,执行默认的表达式
                    trigger = new CronTrigger(getDefaultCron());
                    return trigger.nextExecutionTime(triggerContext);
                }
            });
        }
    	// 获取cron表达式方法,抽象方法,给子类实现
        protected abstract String getCronString();
        // 执行业务操作
        protected abstract void doBusiness();
        // cron表达式报错获取默认的cron表达式
        protected abstract String getDefaultCron();
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    子类实现抽象类,然后实现方法:

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import javax.annotation.Resource;
    
    @Service
    @Slf4j
    public class ItemSyncScheduler extends AbstractScheduler {
    
    	// 默认的cron表达式
        @Value("${configtask.default.itemsync}")
        private String defaultCron ;
    
        @Override
        protected String getCronString() {
        	// 获取数据库里的cron表达式
        	String cronString = "0 0/1 * * * ?";
        	return cronString ;
        }
    
        @Override
        protected void doBusiness() {
            // 执行业务操作
        }
    
        @Override
        protected String getDefaultCron() {
            return defaultCron;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    看起来是没多大问题,不过在定时程序,分布式环境,可能会出现重复执行业务的情况,所以需要加上分布式锁,可以直接使用redission的分布式锁

    加上redisson-spring-boot-starter

    <dependency>
         <groupId>org.redissongroupId>
         <artifactId>redisson-spring-boot-starterartifactId>
         <version>3.16.3version>
     dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    application.yml配置Redis

    spring:
      redis:
        host: 127.0.0.1
        port: 6379
        password: 123456
        database: 8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    修改一下抽象类:

    import cn.hutool.core.thread.NamedThreadFactory;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.DisposableBean;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.scheduling.annotation.SchedulingConfigurer;
    import org.springframework.scheduling.config.ScheduledTaskRegistrar;
    import org.springframework.scheduling.support.CronTrigger;
    
    import java.util.concurrent.ScheduledThreadPoolExecutor;
    import java.util.concurrent.locks.Lock;
    
    @Slf4j
    public abstract class AbstractScheduler implements SchedulingConfigurer, InitializingBean {
    
        private Lock rlock;
    
        @Override
        public void afterPropertiesSet() {
            rlock = getLock();
        }
    
        @Override
        public void configureTasks(ScheduledTaskRegistrar registrar) {
            registrar.addTriggerTask(()->{
                try {
                    // 加分布式锁
                    rlock.lock();
                    // 执行业务
                    doBusiness();
                }finally {
                    // 释放锁
                    rlock.unlock();
                }
            }, triggerContext ->{
                String cron = this.getCronString();
                CronTrigger trigger;
                try {
                    trigger = new CronTrigger(cron);
                    return trigger.nextExecutionTime(triggerContext);
                }catch (Exception e) {
                    log.error("cron表达式异常,已经启用默认配置");
                    // 配置cron表达式异常,执行默认的表达式
                    trigger = new CronTrigger(getDefaultCron());
                    return trigger.nextExecutionTime(triggerContext);
                }
            });
        }
    
        protected abstract String getCronString();
    
        protected abstract void doBusiness();
    
        protected abstract String getDefaultCron();
    
        protected abstract Lock getLock();
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    子类可以根据需要选择锁,可以是单机锁或者分布式锁

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import javax.annotation.Resource;
    
    @Service
    @Slf4j
    public class ItemSyncScheduler extends AbstractScheduler {
    
    	  @Autowired
        private RedissonClient redissonClient;
    
    	// 默认的cron表达式
        @Value("${configtask.default.itemsync}")
        private String defaultCron ;
    
        @Override
        protected String getCronString() {
        	// 获取数据库里的cron表达式
        	String cronString = "0 0/1 * * * ?";
        	return cronString ;
        }
    
        @Override
        protected void doBusiness() {
            // 执行业务操作
        }
    
        @Override
        protected String getDefaultCron() {
            return defaultCron;
        }
    
    	@Override
        protected Lock getLock() {
            return redissonClient.getLock(this.getClass().getSimpleName());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    ps:这个定时程序在下次执行的时候会加载新的时间,所以如果改了定时程序不能马上触发,所以可以进行改进,比如可以使用canal等中间件监听数据表,如果改了就再设置cron表达式,然后触发程序,或者在保存定时程序的时候,调一下重新加载

    基于这版定时程序,改进版可以参考博客,这版定时程序,是在点击保存定时程序时候触发定时程序重新加载

    归纳总结

    项目里合理使用设计模式可以提高代码复用行,代码拓展行好,看起来比较简洁

  • 相关阅读:
    [C语言经典100例题-67】(指针解决)输入数组,最大的与第一个元素交换,最小的与最后一个元素交换,输出数组
    大数据技术之 HBase简介
    如何模拟自然界生态系统中的食物链
    全网最适合入门的面向对象编程教程:20 类和对象的 Python 实现-组合关系的实现与 CSV 文件保存
    在职场上有多少人输在了不会用Python数据分析
    如何“使用Docker快速安装Jenkins,在CentOS7”?
    代码随想录 -- day23 -- ● 669. 修剪二叉搜索树 、108.将有序数组转换为二叉搜索树 、538.把二叉搜索树转换为累加树
    ESP32C3 LuatOS RC522①写入数据并读取M1卡
    具身智能(Embodied AI)
    【ACWing】2725. 数字序列
  • 原文地址:https://blog.csdn.net/u014427391/article/details/127577466