前言
无意间看到别人的博客有个博客统计的系统,大致就是通过调用百度统计的 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 或许是一个更好的选择