@Scheduled
这种方式的定时任务是比较简单而且常见的。
功能不弱,用法也十分方便:
- 开启定时任务功能(两种方式,后续介绍)
- 将任务的方法加上此注解
- 将任务的类交结 Spring 管理 (例如使用 @Component )
先看看这个注解类的总注释:
可以看出主要有四段话,大致传递了 4 个主要信息点:
- 用这个注解时要指定
corn
、fixedDelay
或fixedRate
三个属性中的一个; - 使用这个注解的方法必需无参,无返回值,若有返回值将被忽略
- 该注解通过注册
ScheduledAnnotationBeanPostProcessor
类而运作。这个过程可以手动进行,或者使用更方便的方式:通过
标签在 xml 中配置或者@EnableScheduling
注解 - 此注解可以作为自定义注解的元注解使用
直接看注解源码,去除注释版,更清晰:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;
String cron() default "";
String zone() default "";
long fixedDelay() default -1;
String fixedDelayString() default "";
long fixedRate() default -1;
String fixedRateString() default "";
long initialDelay() default -1;
String initialDelayString() default "";
}
若要详细了解每个属性的作用还是要认真看注释,毕竟此处只为了看整体的结构,就不掉书袋了。
根据源代码的注释与个人理解,几个属性说明如下:
- CRON_DISABLED: 这个变量在
ScheduledAnnotationBeanPostProcessor
中会用作开启 cron 的判断条件 - cron:用于设置 类 cron 表达式(cron-like expression)来描述任务的运行时机
- zone: 设置 cron 所描述的时间的时区,默认为空,此时取的是服务器本地时区
- fixedDelay:用于设置任务的 上一次调用结束后~下一次调用开始前 的时间间隔,单位:毫秒
- fixedDelayString: 参数 fixedDelay 的字符串参数形式,与 fixedDelay 只能二选一使用
- fixedRate:用于设置任务的 两次调用之间 的固定的时间间隔,单位:毫秒
- fixedRateString: 参数 fixedRate 的字符串形式,与 fixedRate 只能二选一使用
- initialDelay: 用于设置在首次执行 fixedRate 或 fixedDelay 任务之前要延迟的 毫秒数
- initialDelayString: 参数 initialDelay 的字符串形式,与 initialDelay 只能二选一使用
cron 表达式
cron 表达式是一种用来描述时间的通用 DSL,类似用来描述字符串模式的正则。
cron 表达式由 6 ~ 7 项组成,中间用空格分开。从左到右依次是:秒、分、时、日、月、周几、年(可选)。值可以是数字,也可以是以下符号:
*:表示匹配所有可能的值
?:仅被用于天(月)和天(星期)两个子表达式,表示不指定值
L:仅被用于天(月)和天(星期)两个子表达式,表示倒数,如 3L 表示倒数第三天,使用时不要指定列表或范围
W:仅被用于天表达式中,表示距该天最近的工作日
,:或者
/:指定数值的增量
-:区间
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒 | 0~59 | , - * / |
分 | 0~59 | , - * / |
小时 | 0~23 | , - * / |
日期 | 1~31 | , - * ? / L W C |
月份 | 1~12 或者 JAN~DEC | , - * / |
星期 | 1~7 或者 SUN~SAT | , - * ? / L C # |
年(可选) | 留空,1970~2099 | , - * / |
常见的表达式:
0 * * * * *:每分钟(当秒为0的时候)
0 0 * * * *:每小时(当秒和分都为0的时候)
*/10 * * * * *:每10秒
0 5/15 * * * *:每小时的5分、20分、35分、50分
0 0 9,13 * * *:每天的9点和13点
1. xml 方式开启定时任务并使用
多见于传统 xml 配置的项目中。
spring.xml 或 application.xml 中添加 task 的命名空间和标签:
在类中使用注解和 cron 表达式即可(下图用的 Kotlin,Java 同理):
2. @EnableScheduling 注解方式开启定时任务并使用
Spring Boot 中使用定时任务更加简单,不需要写 xml,只需要在启动类上添加 @EnableScheduling 即可:
使用任务和上述方式相同
关于任务细节的几个问题
- 若有多个任务,任务是单线程还是多线程执行的?若是单线程,执行顺序是怎样的?
- 设置了 cron,如果任务用时超过了给定的时间间隔将会如何?
- 设置了 fixedRate,如果任务用时超过了设置的 fixedRate 值将会如何?
- 若 1 中结果为单线程,那么可以使用多线程执行任务吗?
如果是强调任务间隔的定时任务,建议使用fixedRate和fixedDelay,如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式
为方便研究,下面均使用上面 Spring Boot 的例子来做测试。
测试一:
-
测试结果一
Result 1
可看出两个结论:1. 多个 job 在同一个线程中执行;有同时执行的多个 job 时,这些 job 的执行顺序不定
测试二:
修改代码如下,增大任务用时至超过任务间隔:
-
测试结果二:
Result 2
可以看出:在一个时间间隔内,如果一轮任务还没跑完,则下一轮任务的执行时间将会推后一个时间间隔,以此类推。
测试三:
如果任务用时超过了设置的 fixedRate 值会是怎样的结果呢?修改测试代码如下:
-
测试结果三:
Result 3
可以看出:若任务用时超过 fixedRate 值,则下一轮任务会紧接在本轮任务后进行。
测试四:
多线程定时任务。
如果只使用默认配置,十分简单,只需要在启动类加上 @EnableAsync,并且在任务类加上 @Async 即可。
也可以自己写配置类如下:
-
测试结果四:
Result 4
可以看出有两个线程在处理这个任务。
定时任务原理
项目启动时,在初始化 bean 后,带 @Scheduled 注解的方法会被拦截,然后依次:构建执行线程,解析参数,加入线程池。
拦截注解的类就是大名鼎鼎的 ScheduledAnnotationBeanPostProcessor
关键的方法如下:
-
postProcessAfterInitialization
postProcessAfterInitialization 代码分析
重点在第 4 步
期中比较引人注目的就是 processScheduled 了
看看里面卖的什么葫芦
里面封装了较多的主要处理逻辑,代码量足足有 122 行(spring-context-5.3.5.jar)
故下面就仅摘抄主要代码,省略的代码用 “...”
- processScheduled,注意我定了 8 个锚点注释
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
// 1. 创建线程
Runnable runnable = createRunnable(bean, method);
...
// 2. 处理 initialDelay 与 initialDelayString 属性
long initialDelay = scheduled.initialDelay();
String initialDelayString = scheduled.initialDelayString();
...
// 3. 处理 cron 与 zone 属性
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
String zone = scheduled.zone();
if (this.embeddedValueResolver != null) {
cron = this.embeddedValueResolver.resolveStringValue(cron);
zone = this.embeddedValueResolver.resolveStringValue(zone);
}
if (StringUtils.hasLength(cron)) {
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
processedSchedule = true;
// 4. 此处用到了注解的第一个属性 CRON_DISABLED
if (!Scheduled.CRON_DISABLED.equals(cron)) {
TimeZone timeZone;
if (StringUtils.hasText(zone)) {
timeZone = StringUtils.parseTimeZoneString(zone);
}
else {
timeZone = TimeZone.getDefault();
}
// 5. 处理重点
// cron 与 zone 构造 CronTrigger,再同方法开始构造好的线程一起构造 CronTask ( Task 的子类 )
// 再将构造好的 CronTask 存到到 ScheduledTaskRegistrar 的 CronTask 列表中
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
}
}
...
// 6. 处理 fixedDelay ,与上述过程类似
long fixedDelay = scheduled.fixedDelay();
...
// 7. 处理 fixedRate,与上述过程类似
long fixedRate = scheduled.fixedRate();
...
// 8. 最后将 task 注册到 scheduledTasks 中
synchronized (this.scheduledTasks) {
Set regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
regTasks.addAll(tasks);
}
}
...
}
上面的 8 点注释中,内容最多的是第 5 点,为方便研究,将它与 registrar 的声明一起拷下来:
private final ScheduledTaskRegistrar registrar;
...
// 5. 处理重点
// cron 与 zone 构造 CronTrigger,再同方法开始构造好的线程一起构造 CronTask ( Task 的子类 )
// 再将构造好的 CronTask 存到到 ScheduledTaskRegistrar 的 CronTask 列表中
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
这段代码的逻辑我已经在注释上写明了,
那么这个看似很重要的 ScheduledTaskRegistrar 是什么东西呢?
从类名上可以看出它的作用是将 Task 注册为 Spring 任务的类,实际上亦可理解为任务线程池与任务的逻辑连接纽带。
这个类又是一大堆东西,不过只需要看她的几个属性就知道她的大概作用了, 顺便贴上几个关键的方法:
public class ScheduledTaskRegistrar implements ScheduledTaskHolder, InitializingBean, DisposableBean {
public static final String CRON_DISABLED = "-";
@Nullable
private TaskScheduler taskScheduler; // 任务调度器,已知其实现有并发调度器和线程池调度器,详情可看源码
@Nullable
private ScheduledExecutorService localExecutor; // 线程池
@Nullable
private List triggerTasks; // 触发式任务
@Nullable
private List cronTasks; // cron任务
@Nullable
private List fixedRateTasks; // 间隔任务
@Nullable
private List fixedDelayTasks; // 间隔任务
private final Map unresolvedTasks = new HashMap<>(16); // 未处理任务
private final Set scheduledTasks = new LinkedHashSet<>(16); // 调度任务
public void setTaskScheduler(TaskScheduler taskScheduler) // 有 setter (且不止一个),可以由外部调用设置任务调度器
...
/**
* 计划指定的 cron 任务,如果可能,立即进行调度,或者在调度程序初始化时进行调度。
* 返回值:
* 计划任务的句柄,允许将其取消(如果处理先前注册的任务,则为 null)
*/
public ScheduledTask scheduleCronTask(CronTask task) {
ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
boolean newTask = false;
if (scheduledTask == null) {
scheduledTask = new ScheduledTask(task);
newTask = true;
}
if (this.taskScheduler != null) {
// 调度任务
scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
}
else {
// 如果没有调度器则将任务放进列表里,同时添加到未处理任务中
addCronTask(task);
this.unresolvedTasks.put(task, scheduledTask);
}
return (newTask ? scheduledTask : null);
}
/**
* 在 bean 构造时调用 scheduleTasks()
*/
@Override
public void afterPropertiesSet() {
scheduleTasks();
}
/**
* 根据基础任务计划程序计划所有已注册的任务
*/
@SuppressWarnings("deprecation")
protected void scheduleTasks() {
// 可见默认设置的线程池是单线程的,如果手动设置 taskScheduler,是可以自己定义线程池的
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
if (this.triggerTasks != null) {
for (TriggerTask task : this.triggerTasks) {
addScheduledTask(scheduleTriggerTask(task));
}
}
if (this.cronTasks != null) {
for (CronTask task : this.cronTasks) {
addScheduledTask(scheduleCronTask(task));
}
}
if (this.fixedRateTasks != null) {
for (IntervalTask task : this.fixedRateTasks) {
addScheduledTask(scheduleFixedRateTask(task));
}
}
if (this.fixedDelayTasks != null) {
for (IntervalTask task : this.fixedDelayTasks) {
addScheduledTask(scheduleFixedDelayTask(task));
}
}
}
}
从上面的代码可以知道,默认的线程池用的是单线程,这也印证了之前的实验结果。