前言

Spring Boot系列: 点击查看Spring Boot系列文章



定时任务

Spring Boot定时任务实现在主要有两种方法:
第一种:使用spring自带的Task任务。使用简单,功能比较单一
第二种:使用Quartz框架。功能强大,使用起来较复杂

以下主要介绍第一种方法,第一种方法对应一般普通的定时任务也够用了



Task开发

由于SpringBoot内置了定时任务Scheduled,所以我们不需要额外的导入依赖。

开启定时任务的方法有两种

一种是在配置类上加@EnableScheduling,然后在配置类中写定时任务方法,任务就会定时执行

下面这个是@EnableScheduling源码中的一个例子:

* @Configuration
 * @EnableScheduling
 * public class AppConfig {
 *
 *     @Scheduled(fixedRate=1000)
 *     public void work() {
 *         // task execution logic
 *     }
 * }

还有一种方法是在启动类上加@EnableScheduling注解,然后spring就会扫描该位置下定时任务并执行。现在一般是使用这种方法,但如果定时任务较少的话,使用第一种方法也可以。

@SpringBootApplication
@EnableScheduling
public class DemoApplication {

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

}



定义定时任务

下面我以第二种方法为例子,看如何定义定时任务

@Component
public class ScheduledTest {

    private static final Logger logger = LoggerFactory.getLogger(ScheduledTest.class);

    @Scheduled(fixedRate = 10000)
    public void fixedRate() {
        ("fixedRate开始执行");
    }
    @Scheduled(fixedDelay = 10000)
    public void fixedDelay() {
        ("fixedDelay开始执行");
    }

    @Scheduled(cron = "0/10 * * * * ?")
    public void cron(){
        ("cron开始执行");
    }

}

输出:

2020-06-17 14:24:28.626  INFO 18132 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedRate开始执行
2020-06-17 14:24:28.628  INFO 18132 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行
2020-06-17 14:24:30.001  INFO 18132 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : cron开始执行
2020-06-17 14:24:38.626  INFO 18132 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedRate开始执行
2020-06-17 14:24:38.630  INFO 18132 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行
2020-06-17 14:24:40.000  INFO 18132 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : cron开始执行

用@Component注解将定时类将给spring管理,这样就能扫描到并执行定时任务。 @Scheduled 注解表示开启一个定时任务,一个@Scheduled 注解的方法就表示了一个定时任务。



@Scheduled注解用来表示定时时间的属性有三个:fixedDelay,fixedRate,cron。

fixedRate 表示任务执行之间的时间间隔,具体是指两次任务的开始时间间隔,即定时任务开始时就开始计时,时间达到fixedRate 设置的时间值,就再次执行任务。

fixedDelay 表示任务执行之间的时间间隔,是从一个定时任务完成之后才开始计时的,达到fixedDelay 设定的值就再次执行,就是指本次任务结束到下次任务开始之间的时间间隔。

fixedRate 和fixedDelay 的区别就是fixedRate 是从一个任务开始执行就开始计时,而fixedDelay 是从一个任务执行完成才开始执行。在一个任务执行较久时,差别就很明显了。
注:fixedRate 和fixedDelay 的时间单位都是毫秒,1秒等于1000毫秒



cron表达式

cron属性对应一个cron表达式

cron表达式有六个或者七个域,每个域表示的时间单位如下:

秒 分钟 小时 日 月 星期 年

注意:在@Scheduled注解中的cron属性采用的是六个域的写法(即没有最后一个表示年的时间域),当输入七个的时候会报表达式错误

下面是一个cron说明表格

springBoot计划任务 springboot自动任务_java

通配符含义:

? 表示不指定值,即不关心某个字段的取值时使用。需要注意的是,月份中的日期和星期可能会起冲突,因此在配置时这两个得有一个是 ?

* 表示所有值,例如:在秒的字段上设置 *,表示每一秒都会触发

, 用来分开多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发

- 表示区间,例如在秒上设置 “10-12”,表示 10,11,12秒都会触发
/ 用于递增触发,如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)

#序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六,(用 在母亲节和父亲节再合适不过了)
周字段的设置,若使用英文字母是不区分大小写的 ,即 MON 与mon相同

L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会自动判断是否是润年), 在周字段上表示星期六,相当于"7"或"SAT"(注意周日算是第一天)。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示"本月最后一个星期五"

W 表示离指定日期的最近工作日(周一至周五),例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发,如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,“W"前只能设置具体的数字,不允许区间”-")
L 和 W 可以一组合使用。如果在日字段上设置"LW",则表示在本月的最后一个工作日触发(一般指发工资 )



cron表达式在线生成网站

推荐一个在线生成cron表达式的网站:https://cron.qqe2.com/ 遇到不会写的cron表达式时,进去直接生成就可以了,妈妈再也不用担心不会写cron表达式了



定时任务的坑

Task单线程问题

从上面我的输出结果,可以看到每次的定时任务都是一个线程执行的(scheduling-1),这是因为spring提供的调度器是默认采用单线程的线程池

这样会导致如果一个定时任务发生阻塞,将会影响其他定时任务的执行,因此我们需要配置多线程执行来解决此问题。

下面这个例子说明但线程执行定时任务会出现说明什么坑

@Component
public class ScheduledTest {

    private static final Logger logger = LoggerFactory.getLogger(ScheduledTest.class);

    @Scheduled(fixedRate = 10000)
    public void fixedRate() throws InterruptedException {
        //让线程睡15秒
        Thread.sleep(15000L);
        ("fixedRate开始执行");
    }
    @Scheduled(fixedDelay = 10000)
    public void fixedDelay() {
        ("fixedDelay开始执行");
    }

}

输出结果:

2020-06-17 15:02:04.424  INFO 4768 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedRate开始执行
2020-06-17 15:02:04.425  INFO 4768 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行
2020-06-17 15:02:19.425  INFO 4768 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedRate开始执行
2020-06-17 15:02:34.425  INFO 4768 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedRate开始执行
2020-06-17 15:02:34.425  INFO 4768 --- [   scheduling-1] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行

我们看结果,除了阻塞了任务,让任务不是指定时间执行的这个问题。还有一个大问题就是,大家看第二次执行定时任务,是少了fixedDelay开始执行这个定时任务输出的,说明fixedDelay这个定时任务没有执行成功。这是因为在第二次执行时,fixedRate睡眠了15秒,已经过了fixedDelay定时的10秒,所以第二个fixedDelay定时任务相当于没有执行。

那么我们就需要将定时任务变为并行执行,方法很简单,配置多个线程执行就可以了,添加配置类,实现SchedulingConfigurer 并覆写configureTasks方法,在该方法中我们将scheduledTask的单线程调度器换成我们自己创建的多线程调度器。具体线程数,要看自己的业务,我这里有两个定时任务,所以配置了两个线程。

@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(2));
    }

}

再次执行上面的代码,输出以下结果

2020-06-17 15:17:40.230  INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行
2020-06-17 15:17:50.233  INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行
2020-06-17 15:17:55.230  INFO 15444 --- [pool-1-thread-1] c.e.d.c.scheduled.ScheduledTest          : fixedRate开始执行
2020-06-17 15:18:00.234  INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行
2020-06-17 15:18:10.230  INFO 15444 --- [pool-1-thread-1] c.e.d.c.scheduled.ScheduledTest          : fixedRate开始执行
2020-06-17 15:18:10.234  INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行
2020-06-17 15:18:20.235  INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest          : fixedDelay开始执行

从结果可以看出,两个定时任务已经分开执行,任务一不能阻塞任务二了。

有童鞋会担心,如果定时任务出错了,跑异常了,定时任务还会执行吗,下面我们就来试试

@Scheduled(fixedDelay = 10000)
    public void fixedDelay() throws InterruptedException {
        ("fixedDelay开始执行");
        throw new InterruptedException();
    }

经过测试得知,当一个任务执行异常,是不影响下一次的执行,下一次还是会继续执行。