从SpringTask开始入手定时任务

SpringTask是spring给我们提供的一种可以实现定时任务的框架。springboot项目可以直接使用,较为方便。但缺点是不好在分布式场景中使用,但是可以将其作为入门定时任务的第一步,由简入难。上篇文章中已经说过了cron表达式的基本用法,这篇文章对springtask进行介绍,尽可能的去解释一些原理,方便后续使用更高级的定时任务框架。

用法一:基于注解的方式实现定时任务。

less 复制代码
@SpringBootApplication
@EnableScheduling
public class TaskdemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(TaskdemoApplication.class, args);
    }

}

@EnableScheduling注解可以理解为定时任务的开关,当将其放在启动类上,会激活spring的定时任务扫描功能。然后还需要用到的注解则是@Scheduled,这个注解与你具体要实现的定时任务所执行的内容相关。被激活的定时任务组件会在程序启动时扫描@Scheduled所在的方法,将他们的方法注册为定时任务,然后按照要求执行。

typescript 复制代码
@Component
public class AnnotationBean {

    @Scheduled(fixedRate = 1000) //固定频率执行
    public void task(){
        System.out.println(new Date());
    }

    @Scheduled(cron = "0 0 0 LW * *") //根据cron表达式执行
    public void task2(){
        System.out.println(new Date());
    }

    @Scheduled(fixedDelay = 1000)  //上一次任务执行多久之后再执行
    public void task3(){
        System.out.println(new Date());
    }

    @Scheduled(initialDelay = 1000, fixedRate = 1000)  //首次执行延迟1秒,后续的任务以固定的间隔执行。
    public void task4(){
        System.out.println(new Date());
    }

}

用法二:基于SchedulingConfigurer接口来实现

第一种方式已经可以实现相对简单的要求了,但还是存在一定的缺陷。比如,我们写在注解上的时间配置要求都是硬编码,一旦项目启动之后就没有办法进行改动了。我经常可以在公司的项目里面看到将cron表达式写在数据库里面的办法,一些为用户提供的定时任务调度,是允许他们对一些定时任务进行修改的。在这种情况下,用法一就无法满足需求了。因为注解里所指定的时间要求,都是spring启动的时候被扫描并注册的,没办法再进行改变。为了能够利用springTask实现动态配置(在不重启服务的情况下修改cron)的要求,可以采用本方法。这里就需要做一些前置铺垫了,先说一些概念性的知识,不然直接看代码也没什么意义。

我们首先来看一下角色划分,在spring的内部都是有哪些组件相互协同来实现定时任务的:

后面的代码重点操作的就是register 和 trigger两个对象,先来看一下详细的协同过程:

可以看到,当register将任务注册之后,其实就不需要在管什么事情了,只需要交给调度器通过trigger获取到执行时间后,再将任务放进执行器。而执行器呢,实际上是一个线程池,默认使用了单线程的调度线程池。线程池大家不陌生吧?调度线程池的任务队列用的是延迟阻塞队列,而延迟阻塞队列的特性就是放进去的任务要等待一定的时间之后才可以拿出来。当然了,这些任务在内部也会被排序。那么回到我们原来的问题,应该在哪个步骤操作才能够实现动态配置呢?答案是trigger,可以从图中看到,一个任务在执行结束之后,会计算下一次任务的执行时间,再次丢到线程池中。而方法一中基于注解的方式无法改变cron的原因就是在注解中写的cron总是不变的,每次计算下一次执行时间的时候都是基于最初的cron,自然也就无法改变。那么,如果我们将cron存储在了数据库中,并且让trigger计算任务下一次执行时间的时候去数据库里面查询最新的cron是不是就可以解决这个问题了?从而也就实现了动态配置cron。来看下面的这么一段代码

typescript 复制代码
@Service
@EnableScheduling
public class TaskService implements SchedulingConfigurer {
    @Autowired
    private TaskStorage taskStorage; //模拟的数据库操作,数据列包括(id, cron)

    private ScheduledTaskRegistrar taskRegistrar; 
    
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        this.taskRegistrar = taskRegistrar;  //获取注册器,方便扩展
        addTasks(); //初始化注册器
    }

    public void addTasks() {
        for(Integer id : taskStorage.ids()) {
            TriggerTask triggerTask = new TriggerTask(new TaskR(id), triggerContext -> {
                String cron = taskStorage.findCron(id); //根据id获取cron表达式
                if(cron == null) {
                    System.out.println("cron is null"); //为null不注册
                }
                return new CronTrigger(cron).nextExecutionTime(triggerContext); //计算下一次的执行时间
            });
            taskRegistrar.addTriggerTask(triggerTask);//注册任务
        }
    }
}

任务定制的简单一些,就让他说一下自己的id和当前的时间

typescript 复制代码
@AllArgsConstructor
public class TaskR implements Runnable {

    private Integer taskId;
    @Override
    public void run() {
        System.out.println("TaskR run : " + taskId +"-----"+ new Date());
    }
}

configureTasks第一次看到别人写这份代码我是一脸MB的,因为当时对于注册、调度、trigger都不理解,自然也就看不明白这些代码的意思。下面,让我来为大家梳理一下。

  1. taskStorage无需多言,可以看看作mapper、jpa等等,总之就是用来从数据库中获取cron表达式的。
  2. 可以看到我们的类上面添加了@EnableScheduling注解,放在类上面也能够开启spring的定时任务功能,不过仅仅针对当前模块内进行任务的扫描,而不是像放在启动类上全局扫描。
  3. 类实现了SchedulingConfigurer的接口,字面意思也就是在此类中可以对定时任务的组件进行一些配置。
  4. 而要求我们必须重载的方法就是configureTasks,此方法会在spring初次启动时执行一次,具体的执行时机是Spring 应用启动时的上下文初始化阶段被自动调用。一开始我还不理解,原来我重写了这个方法之后spring会自己调用它,并且spring还会将它自己的注册器传进来,那么我就可以存一下他的引用,这样之后我不就可以DIY了么,在其他时机不是也可以通过rigister向里面注册一些任务?(so sad,不行。。。)
  5. addTasks就是我们自己定义的逻辑了,第一次执行的时候肯定是要遍历所有的cron表达式,把他们都注册了。TriggerTask,可以理解为他是任务被注册器接收时的数据格式,看下代码:
scala 复制代码
public class TriggerTask extends Task {
    private final Trigger trigger;

    public TriggerTask(Runnable runnable, Trigger trigger) {
        super(runnable);
        Assert.notNull(trigger, "Trigger must not be null");
        this.trigger = trigger;
    }

    public Trigger getTrigger() {
        return this.trigger;
    }
}

它继承了Task类,初始化时接收两个参数,Runnable--->我们已经定义过了TaskR。而trigger则会被保存为该任务的一种属性了,并且是final的,还会断言不能为null噢。看下代码发现,他其实是一个接口,并且要求你必须要实现一个方法,听名字就可以知道,方法里面你要写出怎么获取下一次执行时间的逻辑。

java 复制代码
public interface Trigger {
    @Nullable
    Date nextExecutionTime(TriggerContext triggerContext);
}

而Trigger其实是有实现类的,可以看到在CronTrigger中已经实现了很完备的方法,但是这里的cron表达式也是被指定过不能修改的噢。这也是我们为什么要采用自己造一个匿名 trigger实现类(new TriggerTask(new TaskR(id), triggerContext ->{...})的原因了,因为我们就是要不把cron写死,我们要让它每次都去数据库去一个新的cron来用,即使cron没变。但是只要cron变了,下一次再执行的时候就能拿到最新的了。不然我们也不需要大费周章了,直接rigsiter.addCronTriggertask好了,表达式传进去直接完活。顺便一提,rigsiter可以add很多种任务,比如addCronTriggertask就是直接注册了一个使用了CronTrigger的任务。

TriggerTask 复制代码
                String cron = taskStorage.findCron(id); //根据id获取cron表达式
                if(cron == null) {
                    System.out.println("cron is null"); //为null不注册
                }
                return new CronTrigger(cron).nextExecutionTime(triggerContext); //计算下一次的执行时间
            });

再回头看看我们的这段代码吧。我们重写的方式是在计算下一次的执行时间的时候new一个局部变量CronTrigger,然后让他根据上下文调用内部已经实现好的nextExecutionTime方法,来帮我们计算。而具体的代码已经在上面的Crontask中展示过了。简单总结就是,如果是初次执行就以当前时间为基准。如果之前执行过就要看是否按照预期的时间执行,以上一次实际执行的时间为基准。然后再依据cron表达式来进行计算下一次执行的时间。那么我们DIY的这个trigger,在每次计算下一次执行时间的时候,都会从数据库中再次查找新的cron。闭环!!!PS:11点了,后面再写基于多线程怎么实现,怎么取消已经执行的定时任务,。。先晚安啦,看见这篇文章的你,你是最棒的。。。

相关推荐
烛阴4 小时前
自动化测试、前后端mock数据量产利器:Chance.js深度教程
前端·javascript·后端
.生产的驴4 小时前
SpringCloud 分布式锁Redisson锁的重入性与看门狗机制 高并发 可重入
java·分布式·后端·spring·spring cloud·信息可视化·tomcat
攒了一袋星辰5 小时前
Spring @Autowired自动装配的实现机制
java·后端·spring
我的golang之路果然有问题5 小时前
快速了解GO+ElasticSearch
开发语言·经验分享·笔记·后端·elasticsearch·golang
love530love5 小时前
Windows 下部署 SUNA 项目:虚拟环境尝试与最终方案
前端·人工智能·windows·后端·docker·rust·开源
元闰子5 小时前
走技术路线需要些什么?
后端·面试·程序员
元闰子6 小时前
AI Agent需要什么样的数据库?
数据库·人工智能·后端
知初~6 小时前
SpringCloud
后端·spring·spring cloud
希望20176 小时前
go语言基础|slice入门
后端·golang