Spring 定时任务与原理解析

@Scheduled

这种方式的定时任务是比较简单而且常见的。
功能不弱,用法也十分方便:

  1. 开启定时任务功能(两种方式,后续介绍)
  2. 将任务的方法加上此注解
  3. 将任务的类交结 Spring 管理 (例如使用 @Component )

先看看这个注解类的总注释:


@Scheduled

可以看出主要有四段话,大致传递了 4 个主要信息点:

  1. 用这个注解时要指定 cornfixedDelayfixedRate 三个属性中的一个;
  2. 使用这个注解的方法必需无参,无返回值,若有返回值将被忽略
  3. 该注解通过注册 ScheduledAnnotationBeanPostProcessor 类而运作。这个过程可以手动进行,或者使用更方便的方式:通过 标签在 xml 中配置或者 @EnableScheduling 注解
  4. 此注解可以作为自定义注解的元注解使用

直接看注解源码,去除注释版,更清晰:

@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 "";
}

若要详细了解每个属性的作用还是要认真看注释,毕竟此处只为了看整体的结构,就不掉书袋了。
根据源代码的注释与个人理解,几个属性说明如下:

  1. CRON_DISABLED: 这个变量在 ScheduledAnnotationBeanPostProcessor 中会用作开启 cron 的判断条件
  2. cron:用于设置 类 cron 表达式(cron-like expression)来描述任务的运行时机
  3. zone: 设置 cron 所描述的时间的时区,默认为空,此时取的是服务器本地时区
  4. fixedDelay:用于设置任务的 上一次调用结束后~下一次调用开始前 的时间间隔,单位:毫秒
  5. fixedDelayString: 参数 fixedDelay 的字符串参数形式,与 fixedDelay 只能二选一使用
  6. fixedRate:用于设置任务的 两次调用之间 的固定的时间间隔,单位:毫秒
  7. fixedRateString: 参数 fixedRate 的字符串形式,与 fixedRate 只能二选一使用
  8. initialDelay: 用于设置在首次执行 fixedRate 或 fixedDelay 任务之前要延迟的 毫秒数
  9. 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 的命名空间和标签:

spring.xml

在类中使用注解和 cron 表达式即可(下图用的 Kotlin,Java 同理):


Schedule Class

2. @EnableScheduling 注解方式开启定时任务并使用

Spring Boot 中使用定时任务更加简单,不需要写 xml,只需要在启动类上添加 @EnableScheduling 即可:


Boot Class

使用任务和上述方式相同

关于任务细节的几个问题

  1. 若有多个任务,任务是单线程还是多线程执行的?若是单线程,执行顺序是怎样的?
  2. 设置了 cron,如果任务用时超过了给定的时间间隔将会如何?
  3. 设置了 fixedRate,如果任务用时超过了设置的 fixedRate 值将会如何?
  4. 若 1 中结果为单线程,那么可以使用多线程执行任务吗?

如果是强调任务间隔的定时任务,建议使用fixedRate和fixedDelay,如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式

为方便研究,下面均使用上面 Spring Boot 的例子来做测试。

测试一:

Test 1
  • 测试结果一


    Result 1

可看出两个结论:1. 多个 job 在同一个线程中执行;有同时执行的多个 job 时,这些 job 的执行顺序不定

测试二:
修改代码如下,增大任务用时至超过任务间隔:


Test 2
  • 测试结果二:


    Result 2

可以看出:在一个时间间隔内,如果一轮任务还没跑完,则下一轮任务的执行时间将会推后一个时间间隔,以此类推。

测试三:
如果任务用时超过了设置的 fixedRate 值会是怎样的结果呢?修改测试代码如下:


Test 3
  • 测试结果三:


    Result 3

可以看出:若任务用时超过 fixedRate 值,则下一轮任务会紧接在本轮任务后进行。

测试四:
多线程定时任务。
如果只使用默认配置,十分简单,只需要在启动类加上 @EnableAsync,并且在任务类加上 @Async 即可。

也可以自己写配置类如下:


Test 4
  • 测试结果四:


    Result 4

可以看出有两个线程在处理这个任务。

定时任务原理

项目启动时,在初始化 bean 后,带 @Scheduled 注解的方法会被拦截,然后依次:构建执行线程,解析参数,加入线程池。

拦截注解的类就是大名鼎鼎的 ScheduledAnnotationBeanPostProcessor

关键的方法如下:

  1. postProcessAfterInitialization


    postProcessAfterInitialization 代码分析

重点在第 4 步
期中比较引人注目的就是 processScheduled 了
看看里面卖的什么葫芦
里面封装了较多的主要处理逻辑,代码量足足有 122 行(spring-context-5.3.5.jar)
故下面就仅摘抄主要代码,省略的代码用 “...”

  1. 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));
            }
        }
    }

}

从上面的代码可以知道,默认的线程池用的是单线程,这也印证了之前的实验结果。

你可能感兴趣的:(Spring 定时任务与原理解析)