SpringBoot系列之动态定时程序

Posted smileNicky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot系列之动态定时程序相关的知识,希望对你有一定的参考价值。

业务场景

最近需要实现一个功能,根据页面选择的星期,默认是凌晨执行,生成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();



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

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;
    


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

加上redisson-spring-boot-starter

<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson-spring-boot-starter</artifactId>
     <version>3.16.3</version>
 </dependency>

application.yml配置Redis

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 8

修改一下抽象类:

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();



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

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());
    


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

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

归纳总结

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

以上是关于SpringBoot系列之动态定时程序的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot系列之动态定时程序

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

SpringBoot系列之动态定时程序改进版

SpringBoot系列之动态定时程序改进版

SpringBoot系列之动态定时程序改进版

SpringBoot之scheduled定时器