当前位置:首页 > 科技  > 软件

Spring中Cron表达式的优雅实现方案

来源: 责编: 时间:2024-03-18 09:42:48 121观看
导读在 SpringBoot 项目中,我们可以通过@EnableScheduling注解开启调度任务支持,并通过@Scheduled注解快速地建立一系列定时任务。@Scheduled支持下面三种配置执行时间的方式:cron(expression):根据Cron表达式来执行。fixedDe

在 SpringBoot 项目中,我们可以通过@EnableScheduling注解开启调度任务支持,并通过@Scheduled注解快速地建立一系列定时任务。qDU28资讯网——每日最新资讯28at.com

@Scheduled支持下面三种配置执行时间的方式:qDU28资讯网——每日最新资讯28at.com

  • cron(expression):根据Cron表达式来执行。
  • fixedDelay(period):固定间隔时间执行,无论任务执行长短,两次任务执行的间隔总是相同的。
  • fixedRate(period):固定频率执行,从任务启动之后,总是在固定的时刻执行,如果因为执行时间过长,造成错过某个时刻的执行(晚点),则任务会被立刻执行。

最常用的应该是第一种方式,基于Cron表达式的执行模式,因其相对来说更加灵活。qDU28资讯网——每日最新资讯28at.com

可变与不可变

默认情况下,@Scheduled注解标记的定时任务方法在初始化之后,是不会再发生变化的。Spring 在初始化 bean 后,通过后处理器拦截所有带有@Scheduled注解的方法,并解析相应的的注解参数,放入相应的定时任务列表等待后续统一执行处理。到定时任务真正启动之前,我们都有机会更改任务的执行周期等参数。qDU28资讯网——每日最新资讯28at.com

换言之,我们既可以通过application.properties配置文件配合@Value注解的方式指定任务的Cron表达式,亦可以通过CronTrigger从数据库或者其他任意存储中间件中加载并注册定时任务。这是 Spring 提供给我们的可变的部分。qDU28资讯网——每日最新资讯28at.com

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

创造与毁灭

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

先对任务做个简单的抽象,方便统一的识别和管理:qDU28资讯网——每日最新资讯28at.com

public interface IPollableService {    /**     * 执行方法     */    void poll();    /**     * 获取周期表达式     *     * @return CronExpression     */    default String getCronExpression() {        return null;    }    /**     * 获取任务名称     *     * @return 任务名称     */    default String getTaskName() {        return this.getClass().getSimpleName();    }}

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

@Configuration@EnableAsync@EnableSchedulingpublic class SchedulingConfiguration implements SchedulingConfigurer, ApplicationContextAware {    private static final Logger log = LoggerFactory.getLogger(SchedulingConfiguration.class);    private static ApplicationContext appCtx;    private final ConcurrentMap<String, ScheduledTask> scheduledTaskHolder = new ConcurrentHashMap<>(16);    private final ConcurrentMap<String, String> cronExpressionHolder = new ConcurrentHashMap<>(16);    private ScheduledTaskRegistrar taskRegistrar;    public static synchronized void setAppCtx(ApplicationContext appCtx) {        SchedulingConfiguration.appCtx = appCtx;    }    @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        setAppCtx(applicationContext);    }    @Override    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {        this.taskRegistrar = taskRegistrar;    }    /**     * 刷新定时任务表达式     */    public void refresh() {        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 = new CronTask(task::poll, expression);            ScheduledTask scheduledTask = taskRegistrar.scheduleCronTask(cronTask);            if (scheduledTask != null) {                log.info("定时任务[{}]已加载,当前任务表达式为[{}]", taskName, expression);                scheduledTaskHolder.put(beanName, scheduledTask);                cronExpressionHolder.put(beanName, expression);            }        });    }}

重点是保存ScheduledTask对象的引用,它是控制任务启停的关键。而表达式“-”则作为一个特殊的标记,用于禁用某个定时任务。qDU28资讯网——每日最新资讯28at.com

当然,禁用后的任务通过重新赋予新的 Cron 表达式,是可以“复活”的。完成了上面这些,我们还需要一个定时任务来动态监控和刷新定时任务配置:qDU28资讯网——每日最新资讯28at.com

@Componentpublic class CronTaskLoader implements ApplicationRunner {    private static final Logger log = LoggerFactory.getLogger(CronTaskLoader.class);    private final SchedulingConfiguration schedulingConfiguration;    private final AtomicBoolean appStarted = new AtomicBoolean(false);    private final AtomicBoolean initializing = new AtomicBoolean(false);    public CronTaskLoader(SchedulingConfiguration schedulingConfiguration) {        this.schedulingConfiguration = schedulingConfiguration;    }    /**     * 定时任务配置刷新     */    @Scheduled(fixedDelay = 5000)    public void cronTaskConfigRefresh() {        if (appStarted.get() && initializing.compareAndSet(false, true)) {            log.info("定时调度任务动态加载开始>>>>>>");            try {                schedulingConfiguration.refresh();            } finally {                initializing.set(false);            }            log.info("定时调度任务动态加载结束<<<<<<");        }    }    @Override    public void run(ApplicationArguments args) {        if (appStarted.compareAndSet(false, true)) {            cronTaskConfigRefresh();        }    }}

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

验证

我们创建一个原型工程和三个简单的定时任务来验证下,第一个任务是执行周期固定的任务,假设它的Cron表达式永远不会发生变化,像这样:qDU28资讯网——每日最新资讯28at.com

@Servicepublic class CronTaskBar implements IPollableService {    @Override    public void poll() {        System.out.println("Say Bar");    }    @Override    public String getCronExpression() {        return "0/1 * * * * ?";    }}

第二个任务是一个经常更换执行周期的任务,我们用一个随机数发生器来模拟它的善变:qDU28资讯网——每日最新资讯28at.com

@Servicepublic class CronTaskFoo implements IPollableService {    private static final Random random = new SecureRandom();    @Override    public void poll() {        System.out.println("Say Foo");    }    @Override    public String getCronExpression() {        return "0/" + (random.nextInt(9) + 1) + " * * * * ?";    }}

第三个任务就厉害了,它仿佛就像一个电灯的开关,在启用和禁用中反复横跳:qDU28资讯网——每日最新资讯28at.com

@Servicepublic class CronTaskUnavailable implements IPollableService {    private String cronExpression = "-";    private static final Map<String, String> map = new HashMap<>();    static {        map.put("-", "0/1 * * * * ?");        map.put("0/1 * * * * ?", "-");    }    @Override    public void poll() {        System.out.println("Say Unavailable");    }    @Override    public String getCronExpression() {        return (cronExpression = map.get(cronExpression));    }}

如果上面的步骤都做对了,日志里应该能看到类似这样的输出:qDU28资讯网——每日最新资讯28at.com

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

小结

我们在上文通过定时刷新和重建任务的方式来实现了动态更改Cron表达式的需求,能够满足大部分的项目场景,而且没有引入quartzs等额外的中间件,可以说是十分的轻量和优雅了。qDU28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-76560-0.htmlSpring中Cron表达式的优雅实现方案

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 我们一起聊聊如何保证接口幂等性?高并发下的接口幂等性如何实现?

下一篇: Spring事件如何异步执行?

标签:
  • 热门焦点
  • Raft算法:保障分布式系统共识的稳健之道

    Raft算法:保障分布式系统共识的稳健之道

    1. 什么是Raft算法?Raft 是英文”Reliable、Replicated、Redundant、And Fault-Tolerant”(“可靠、可复制、可冗余、可容错”)的首字母缩写。Raft算法是一种用于在分布式系统
  • 三言两语说透设计模式的艺术-简单工厂模式

    三言两语说透设计模式的艺术-简单工厂模式

    一、写在前面工厂模式是最常见的一种创建型设计模式,通常说的工厂模式指的是工厂方法模式,是使用频率最高的工厂模式。简单工厂模式又称为静态工厂方法模式,不属于GoF 23种设计
  • 使用LLM插件从命令行访问Llama 2

    使用LLM插件从命令行访问Llama 2

    最近的一个大新闻是Meta AI推出了新的开源授权的大型语言模型Llama 2。这是一项非常重要的进展:Llama 2可免费用于研究和商业用途。(几小时前,swyy发现它已从LLaMA 2更名为Lla
  • 这款新兴工具平台,让你的电脑效率翻倍

    这款新兴工具平台,让你的电脑效率翻倍

    随着信息技术的发展,我们获取信息的渠道越来越多,但是处理信息的效率却成为一个瓶颈。于是各种工具应运而生,都在争相解决我们的工作效率问题。今天我要给大家介绍一款效率
  • 一个注解实现接口幂等,这样才优雅!

    一个注解实现接口幂等,这样才优雅!

    场景码猿慢病云管理系统中其实高并发的场景不是很多,没有必要每个接口都去考虑并发高的场景,比如添加住院患者的这个接口,具体的业务代码就不贴了,业务伪代码如下:图片上述代码有
  • “又被陈思诚骗了”

    “又被陈思诚骗了”

    作者|张思齐 出品|众面(ID:ZhongMian_ZM)如今的国产悬疑电影,成了陈思诚的天下。最近大爆电影《消失的她》票房突破30亿断层夺魁暑期档,陈思诚再度风头无两。你可以说陈思诚的
  • OPPO、vivo、小米等国内厂商Q2在印度智能手机市场份额依旧高达55%

    OPPO、vivo、小米等国内厂商Q2在印度智能手机市场份额依旧高达55%

    7月20日消息,据外媒报道,研究机构的报告显示,在全球智能手机出货量同比仍在下滑的大背景下,印度这一有潜力的市场也未能幸免,出货量同比也有下滑,多家厂
  • 引领旗舰级影像能力向中端机普及 OPPO K11 系列发布 1799 元起

    引领旗舰级影像能力向中端机普及 OPPO K11 系列发布 1799 元起

    7月25日,OPPO正式发布K系列新品—— OPPO K11 。此次 K11 在中端手机市场长期被忽视的影像板块发力,突破性地搭载索尼 IMX890 旗舰大底主摄,支持 OIS
  • Windows 11发布,微软一改往常对老机型开放的态度

    Windows 11发布,微软一改往常对老机型开放的态度

    距离 Windows 11 发布已经过去一周,在过去一周里,很多数码爱好者围绕其对 Android 应用的支持、对老机型的升级问题展开了激烈讨论。与以往不同的是,在这次大
Top