通过 Spring Task 实现定时任务

技术学习 / 2022-09-19

前言

无意间看到别人的博客有个博客统计的系统,大致就是通过调用百度统计的 api 获取网站相关统计数据,并通过 echarts 图表工具进行可视化分析,在写相关后端程序时便有了个定时任务的需求,如何定时调用百度统计的 api 获取相关数据,大致查了下发现有三种工具可供使用:spring task、quartz、xxl-job,原作者的程序中使用的是 xxl-job 进行调度,xxl-job 的官方文档对于如何使用这个工具也介绍的十分详细,xxl-job 不仅需要在后端程序中引入相关依赖并配置相应的执行器,也需要搭建一个可视化的调度中心,由于 xxl-job 主用于分布式平台,用在我这博客上进行一个定时任务多少有点大材小用了,所以最后选择了最简单的 spring schedule 进行定时任务的处理

Spring Task

启用 Spring Task

在 springboot 项目中,首先便是内置了 spring task 这个定时任务工具,使用也很简单,只需要在启动类上使用 @EnableScheduling 注解即可:

@EnableScheduling
@SpringBootApplication
public class HaloStatisticApplication {
    public static void main(String[] args) {
        SpringApplication.run(HaloStatisticApplication.class, args);
    }
}

注解实现定时任务

定义一个 Spring Bean,在具体实现方法上添加 @Scheduled 注解即可:

@Component
public class BaiduStatistics {
    @Resource
    TokenService tokenService;
    
    @Resource
    SiteService siteService;

    @Scheduled(cron = "0 0 4 ? * MON")
    public void refreshToken() {
        BaiduToken baiduToken = tokenService.refreshToken();
        tokenService.saveToken(baiduToken);
    }
    
    @Scheduled(cron = "0 0 4 * * ?")
    public void refreshSites() {
        List<Site> sites = siteService.refreshSites();
        siteService.saveSites(sites);
    }
}

这样便实现了一个简单的定时任务

Cron 表达式

Cron 表达式是一个具有时间含义的字符串,以空格隔开,分为 6 ~ 7 个域,年域可以不必须

是否必需 取值范围 特殊字符
[0, 59] * , - /
分钟 [0, 59] * , - /
小时 [0, 23] * , - /
日期 [1, 31] * , - / ? L W
月份 [1, 12]或[JAN, DEC] * , - /
星期 [1, 7]或[MON, SUN]。若使用[1, 7]表达方式,1代表星期一,7代表星期日。 * , - / ? L #
[当前年份,2099] * , - /
特殊字符 含义 示例
* 所有可能的值。 在月域中,*表示每个月;在星期域中,*表示星期的每一天。
, 列出枚举值。 在分钟域中,5,20表示分别在5分钟和20分钟触发一次。
- 范围。 在分钟域中,5-20表示从5分钟到20分钟之间每隔一分钟触发一次。
/ 指定数值的增量。 在分钟域中,0/15表示从第0分钟开始,每15分钟。在分钟域中3/20表示从第3分钟开始,每20分钟。
? 不指定值,仅日期和星期域支持该字符。 当日期或星期域其中之一被指定了值以后,为了避免冲突,需要将另一个域的值设为?
L 单词Last的首字母,表示最后一天,仅日期和星期域支持该字符。说明 指定L字符时,避免指定列表或者范围,否则,会导致逻辑问题。 在日期域中,L表示某个月的最后一天。在星期域中,L表示一个星期的最后一天,也就是星期日(SUN)。如果在L前有具体的内容,例如,在星期域中的6L表示这个月的最后一个星期六。
W 除周末以外的有效工作日,在离指定日期的最近的有效工作日触发事件。W字符寻找最近有效工作日时不会跨过当前月份,连用字符LW时表示为指定月份的最后一个工作日。 在日期域中5W,如果5日是星期六,则将在最近的工作日星期五,即4日触发。如果5日是星期天,则将在最近的工作日星期一,即6日触发;如果5日在星期一到星期五中的一天,则就在5日触发。
# 确定每个月第几个星期几,仅星期域支持该字符。 在星期域中,4#2表示某月的第二个星期四。

如上代码,如果我需要在每个月的每周一上午四点对 token 进行一个刷新,那么我的 cron 表达式为 0 0 4 ? * MON,日期和星期可能会有冲突,如果我需要指定星期几的话,日期就需要选择 ? , 如果我需要在每天早上四点对网站统计数据进行刷新,那么我的 cron 表达式为 0 0 4 * * ? ,同样 ? 是为了防止日期和星期冲突,所以两个只需要指定一个

如果实在不会写的话,网上也有很多现成的生成工具,可以直接预测该定时任务之后的执行时刻

@Scheduled 注解参数

参数 作用
cron cron 表达式,指定任务在特定时间运行
fixedDelay 表示上一次任务执行完成后多久再次执行,参数类型为 long,单位 ms
fixedDelayString 同上,参数类型改为 String
fixedRate 表示通过一定频率执行任务,参数类型为 long,单位 ms
fixedRateString 同上,参数类型改为 String
initialDelay 表示延迟多久再一次执行任务,参数类型为 long,单位 ms
initialDelayString 同上,参数类型改为 String
zone 时区,默认为当前时区,一般用不到

多线程支持

spring task 默认为单线程运行,指的是如果多个任务在同一时刻运行的时候会发生堵塞,而在大多数情况下,我们希望同时运行多个任务,故需要开启多线程支持,开启的方法也很简单,通过自定义一个配置类,在其中配置一个 TaskScheduler 即可,Spring 已有默认实现

@Configuration
public class ScheduleConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(20); // 设置线程池的大小为20
        return scheduler;
    }
}

总结

对于 XXL-JOB,也只是浅学了一下如何设置定时任务,这里就不再详述,具体可参见官方文档,同时 quartz 也在 springboot2.X 版本进行了内置,可以更加方便的使用,当然也可以使用 spring task + redis 分布式锁实现任务调度等等,这些都是分布式的任务调度平台,对于个人的小项目来说,用分布式调度平台多少有些浪费,不需要太多东西,只需要一个定时任务,需要精简小巧的话,spring task 或许是一个更好的选择