基于Spring内置任务调度SchedulingConfigurer 实现添加、取消、重置

967 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情

引言

使用Spring的任务调度实现定时任务能够给我们的开发带来了极大的便利,不过当我们的任务调度配置完成后,需要在之前设定下的下次任务执行新的cron表达式,新的配置才能生效,做不到立即生效。如果要立即生效就只能停止服务器。修改源码,再重启服务器等一连串繁琐的操作。下面为大家提供一种Spring内置任务调度实现添加、取消、重置的方法。

传统scheduledTaskRegistrar.addTriggerTask()的缺陷

我们先看看使用scheduledTaskRegistrar.addTriggerTask()方法的代码

scheduledTaskRegistrar.addTriggerTask(baskTask,sheduledConfig->{ 
     Date date = new CronTrigger(baskTask.getCron()).nextExecutionTime(sheduledConfig); 
     return date; 
});

使用scheduledTaskRegistrar.addTriggerTask()确实可以帮我们实现定时任务,但是也存在上述缺陷,需要等定时任务下次执行获取到最新的cron表达式才可以实现修改调度的时间。于是,这种方法不太推荐使用,我们需要一种可添加可删除可修改的方法去实现新的时间调度。

解决思路

我们可以基于 SchedulingConfigurer 的源码,捕获 ScheduledTaskRegistrar 类的实例,通过该类中的 TaskScheduler 实例操作定时任务的增删,而非采用 ScheduledTaskRegistrar.addTriggerTask 方法维护定时任务。 通过查看一下ScheduledTaskRegistrar源码,我们发现该对象初始化完成后会执行scheduleTasks()方法,在该方法中添加任务调度信息,最终所有的任务信息都存放在名为scheduledFutures的集合中。 再通过查看scheduledFutures的方法

image.png 我们可以清楚的看到cancel()的方法。所以,我们可以将所有的feature统一存在一个map中,再通过任务的名字去获取对应的feature,实现任务调度的取消和增加

/**
 * 定时任务类
 * @author lyxrunrunrun
 * Date:2022/9/26 10:34
 */
@EnableScheduling
@Slf4j
@Configuration
public class TaskConfig implements SchedulingConfigurer {
    @Autowired
    private ApplicationContext applicationContext;


    // 句柄,方便后期获取 future
    private TaskScheduler taskScheduler;

    private static final Map<String, ScheduledFuture> FUTURE_MAP = new ConcurrentHashMap<>();

    /**
     * 线程池任务调度器
     * <p>
     * 支持注解方式,@Scheduled(cron = "0/5 * * * * ?")
     */
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(Runtime.getRuntime().availableProcessors() / 3 + 1);
        scheduler.setThreadNamePrefix("TaskScheduler-");
        scheduler.setRemoveOnCancelPolicy(true);  // 保证能立刻丢弃运行中的任务

        taskScheduler = scheduler; // 获取 句柄,方便后期获取 future

        return scheduler;
    }
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {

        scheduledTaskRegistrar.setScheduler(taskScheduler()); // 不用担心,这里的scheduler跟 上面的注解 Bean 是同一个对象,亲自打断点验证

        //获取所有的定时任务
        Map<String, BaskTask> map = applicationContext.getBeansOfType(BaskTask.class);

        //遍历注册
        for(String key :map.keySet()){

            BaskTask baskTask = map.get(key);
            //baskTask:回调内部的run方法
            //baskTask.getCron() 获取各个任务的执行频率
//             scheduledTaskRegistrar.addTriggerTask 不推荐,因为无法获取 调度任务的 future 对象。
            ScheduledFuture<?> schedule = taskScheduler.schedule(baskTask,sheduledConfig->{
                Date date = new CronTrigger(baskTask.getCron()).nextExecutionTime(sheduledConfig);
                return date;
            });
            FUTURE_MAP.put(key, schedule);
        }
    }

    //取消限时任务
    public void cancel(String type){
        if (!FUTURE_MAP.containsKey(type)) {
            return;
        }
        //BUG 修复
        ScheduledFuture future = FUTURE_MAP.get(type);
        if (future != null) {
            future.cancel(true);
        }
        System.out.println("删除成功");
    }

    //重启该限时任务
    public void addTask(String type){
        //获取所有的定时任务
        Map<String, BaskTask> map = applicationContext.getBeansOfType(BaskTask.class);

        BaskTask baskTask = map.get(type);
            //baskTask:回调内部的run方法
            //baskTask.getCron() 获取各个任务的执行频率
//             scheduledTaskRegistrar.addTriggerTask 不推荐,因为无法获取 调度任务的 future 对象。
            ScheduledFuture<?> schedule = taskScheduler.schedule(baskTask,sheduledConfig->{
                Date date = new CronTrigger(baskTask.getCron()).nextExecutionTime(sheduledConfig);
                return date;
            });

            FUTURE_MAP.put(type, schedule);
        System.out.println("修改成功");
    }

    //修改限时任务
    public void updateTask(String type){
        //取消该定时任务
        cancel(type);
        //重启该定时任务
        addTask(type);
    }
}

通过这样的配置就可以实现对任务的修改和删除了,相比scheduledTaskRegistrar.addTriggerTask需要自行写代码维护定时任务列表,控制任务的删减,代码的实现比较繁琐,但也能很方便的实现定时任务的调度。

BaskTask源码

public interface BaskTask extends Runnable {
    //获取执行频率
    String getCron();
    //执行任务逻辑
    void execute();
}

TaskOne源码

@Slf4j
@Component
@Data
public class TaskOne implements BaskTask {

    
    private String cron;

   //外部获取执行频率
    @Override
    public String getCron() {
        return cron;
    }
    
    @Override
    public void execute() {
         //可写复杂的任务逻辑
         System.out.println("执行定时任务");
    }
    //执行任务
    @Override
    public void run() {
        execute();
    }
}

查看结果

通过ApiFox发送请求

image.png

这样我们就实现了动态任务的添加、取消、重置。通过查看源代码的方式实现,对于初学者有一定难度,需要积极而且耐心的试错,如有更好的解决方案,欢迎指正。