Spring Boot - 定时任务

前言

对于一些周期性的工作,使用定时任务来执行是非常合适的操作。

Spring 3.0 时对定时任务提供了支持,其提供了一个接口TaskScheduler作为定时任务的抽象,并且提供了一个默认的实现ThreadPoolTaskScheduler,该实现是对 JDK ScheduledExecutorService的包装并增加了一些扩展触发功能。

本文主要介绍下在 Spring Boot 中使用定时任务。

基本使用

在 Spring Boot 中,要使用定时任务,只需进行如下两步操作:

  1. 使用注解@EnableScheduling开启定时任务:

    @SpringBootApplication
    @ComponentScan("com.yn.scheduled")
    @EnableScheduling // 开启定时任务
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    EnableScheduling注解会注册一个ScheduledAnnotationBeanPostProcessor,从而使能扫描 Spring IOC 容器中对象的@Scheduled注解方法。

  2. 为需要定期执行的方法添加@Scheduled注解:

    @Component // 添加到 IOC 容器中
    public class ScheduledTask {
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Scheduled(fixedRate = 1000)
        public void scheduledTask() {
            logger.info("定时任务(每秒一次):{}",new Date()) ;
        }
    }
    

    上述@Scheduled注解定义了一个每 1 秒执行一次的定时任务scheduledTask

以上,我们就完成了一个定时任务scheduledTask,此时运行程序,就可以在控制台看到定时任务scheduledTask每 1 秒都会得到执行。

:被@Scheduled注解的定时任务有如下限制:

  • 定时方法必须是无参函数
  • 定时方法通常返回void,如果设置了返回其他类型的数据,则返回值会被忽略。

@Scheduled 简解

通过上面内容,我们可以知道,在 Spring 中,定时任务的设置主要通过注解@Scheduled来定义,其具体内容如下图所示:

Scheduled

以下我们只对@Scheduled注解常用的属性进行介绍:

  • cron:表示以 Unix 的 cron 方式定义时间。
    cron的定时设置功能十分灵活强大,具体的设置方式可参考 附录 - cron 表达式

  • fixedRate:表示以固定间隔执行定时任务。这里的间隔指的是:每次调用的时候就开始计时,到指定间隔时间再调用下一个定时任务。

  • fixedDelay:表示以固定间隔执行定时任务。这里的间隔指的是:上一次定时任务完成后,才开始计时,到指定间隔时间再调用下一个定时任务。

  • initialDelay:表示首次运行定时任务前的延时时间。可用在于fixedRatefixedDelay的定时任务。
    initialDelay只有在第一次运行定时任务前有效,不会对后续定时任务有影响。

:以上属性对应的字符串属性(比如,fixedRate对应的字符串属性为fixedRateString),其作用是一样的,只是字符串属性可以从外部文件中进行配置,比如可以把定时任务写到配置文件中,然后在代码中使用:

  1. 配置文件Application.yml
scheduler:
  fixedRate:
    timeInMilliseconds: 3000
  1. 代码中引入配置文件配置:
@Scheduled(fixedRateString = "${scheduler.fixedRate.timeInMilliseconds}")
public void taskFromExternal(){
    logger.info("Thread[{}] - taskFromExternal:{}", Thread.currentThread().getName(), new Date());
}

并发调度定时任务

需要注意的一点是,默认情况下,定时任务是串行运行的(具体原因可参考后文内容:源码分析),因此,哪怕即使使用的是fixedRate,也有可能因为定时任务内部存在耗时操作等行为导致调度出现误差,比如当该耗时操作时间超过定时任务间隔时,就会导致系统中的定时任务调度间隔不准确。比如如下例子:

@Scheduled(fixedRate = 2000)
public void task01() throws InterruptedException {
    // 模拟耗时操作
    Thread.sleep(3000);
    logger.info("Thread[{}] - task01:{}", Thread.currentThread().getName(),new Date());
}

@Scheduled(fixedRate = 5000)
public void task02() {
    logger.info("Thread[{}] - task02:{}", Thread.currentThread().getName(),new Date());
}

运行程序,可以看到如下结果:

可以看到,对于task01,我们设置的调度间隔是 2 秒,时间的运行间隔为 3 秒,原因就是定时任务task01内部耗时操作超过了本身的调度时间间隔,而又由于 Spring 定时任务默认是串行运行,从而也影响了系统中其他定时任务,比如定时任务task02设置的调度时间为 5 秒,但实际运行间隔却为 6 秒。

为了减少系统中的定时任务互相影响,最好让定时任务都运行在独立的线程中,也即并发运行定时任务,这样,即使其中一个或多个定时任务出问题,也不会影响到系统其他定时任务。

一个很方便的事是,刚好 Spring 也提供了异步功能支持,我们之前的文章也进行了介绍:Spring Boot - 执行异步任务

简单来讲,对于定时任务,Spring 提供了TaskScheduler接口进行抽象,同时借助注解@EnableScheduling@Scheduled就可以开启并设置定时任务。
而对于异步任务,Spring 同样提供了一个接口TaskExecutor进行抽象,同时借助注解@EnableAsync@Async就可以开启并设置异步任务。

所以要将定时任务设置为并发调度,只需开启异步任务并为其增添异步执行即可,如下所示:

  1. 开启异步任务支持:
@Configuration
@EnableAsync // 开启异步任务
public class AsyncConfigure implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        return Executors.newCachedThreadPool();
    }
}
  1. 为定时任务增加异步执行功能:
@Scheduled(fixedRate = 2000)
@Async // 异步定时任务
public void task01() throws InterruptedException {
    // 模拟耗时操作
    Thread.sleep(3000);
    logger.info("Thread[{}] - task01:{}", Thread.currentThread().getName(), new Date());
}

@Scheduled(fixedRate = 5000)
@Async // 异步定时任务
public void task02() {
    logger.info("Thread[{}] - task02:{}", Thread.currentThread().getName(), new Date());
}

此时运行程序,结果如下图所示:

可以看到,异步任务的调度间隔符合我们的设置,即使对于自身内部运行超过定时间隔的任务时,也会新开一条线程执行新的定时任务,不会由于上一个任务的超时而导致系统中其他定时任务受到影响。

:Spring 对异步任务和定时任务的抽象和实现十分类似,比如对于异步任务,使用TaskExecutor进行抽象,且只需提供@EnableAsync@Async就可以开启并设置异步任务,而对于定时任务,也是同样的套路,使用TaskScheduler进行抽象,且只需提供@EnableScheduling@Scheduled就可以开启并设置定时任务...
在我们之前的文章(Spring Boot - 执行异步任务)中有提及到,Spring 提供了一个接口AsyncConfigurer可以让我们对异步任务进行更加细粒度的设置,同样的,对于定时任务,Spring 也提供了一个类似的接口SchedulingConfigurer,可以让我们对定时任务进行更加细粒度的设置,有时候使用@Scheduled注解无法解决的问题,比如动态改变定时时间等,就可以通过SchedulingConfigurer进行配置。这里,对于定时任务并发调度,我们也可以通过实现该接口进行实现,如下代码所示:

@Configuration
@EnableScheduling
public class ScheduledConfigure implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
    }

    @Bean
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(100);
    }
}

这样配置后,系统中所有的定时任务无需添加@Async就会自动运行在线程池中。

:这里需要注意的一个点是,ScheduledTaskRegistrar.setScheduler方法只支持TaskSchedulerScheduledExecutorService,因此这里不是采用前文的Executors.newCachedThreadPool,而是使用Executors.newScheduledThreadPool,这两者的效果有一点不同,具体运行结果如下所示:

可以看到,定时任务task01设置的调度间隔是 2 秒,实际时间却是 3 秒,出现这种状况的原因是ScheduledExecutorService.scheduleAtFixedRate方法的时间间隔是上一次运行成功后,才开始计时,也就是,ScheduledExecutorService的执行方式是fixedDelay模式,因此,只有在前一个任务结束后,才会开启下一个任务,所以就导致了上述现象,即不同任务可以并发调度,但是同一个任务只能串行调度,所以如果这种效果不是你所期望的,则应当采用上文介绍的结合异步任务来完成定时任务并发调度。

源码分析

这里我们简单对定时任务的整个过程做一个简要分析,如下所示:

首先,Spring 中的定时任务是通过注解@EnableScheduling开启的,所以查看下该注解源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

很明显看到,@EnableScheduling注解的主要操作就是导入了一个配置类SchedulingConfiguration.class,查看下该配置类源码:

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

    @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }
}

SchedulingConfiguration配置类主要作用就是创建了一个ScheduledAnnotationBeanPostProcessor的 Bean 实例,其源码如下所示:

public class ScheduledAnnotationBeanPostProcessor
        implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,ApplicationListener<ContextRefreshedEvent>,... {

    // 构造函数
    public ScheduledAnnotationBeanPostProcessor() {
        this.registrar = new ScheduledTaskRegistrar();
    }

    // 扫描 @Scheduled 注解并创建相应的方法实例
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        ...
        AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
    Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
            (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
                Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                        method, Scheduled.class, Schedules.class);
                return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
            });
        ...
        // Non-empty set of methods
        annotatedMethods.forEach((method, scheduledMethods) ->
                scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
        ...
    }

    // 解析 @Scheduled 并封装定时任务到 ScheduledTaskRegistrar
    protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
        ...
        Set<ScheduledTask> tasks = new LinkedHashSet<>(4);

        // Determine initial delay
        long initialDelay = scheduled.initialDelay();
        String initialDelayString = scheduled.initialDelayString();
        ...
        // Check cron expression
        String cron = scheduled.cron();
        if (StringUtils.hasText(cron)) {
            ...
            tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
            }
        }
        ...
        // Check fixed delay
        long fixedDelay = scheduled.fixedDelay();
        if (fixedDelay >= 0) {
            ...
            tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
        }
        String fixedDelayString = scheduled.fixedDelayString();
        if (StringUtils.hasText(fixedDelayString)) {
            ...
            tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
            }
        }
        // Check fixed rate
        long fixedRate = scheduled.fixedRate();
        if (fixedRate >= 0) {
            ...
            tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
        }
        String fixedRateString = scheduled.fixedRateString();
        if (StringUtils.hasText(fixedRateString)) {
            ...
            tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
            }
        }

        // Check whether we had any attribute set
        Assert.isTrue(processedSchedule, errorMessage);

        // Finally register the scheduled tasks
        synchronized (this.scheduledTasks) {
            Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
            regTasks.addAll(tasks);
        }
        ...
    }
}

ScheduledAnnotationBeanPostProcessor创建的时候首先会构造一个ScheduledTaskRegistrar实例,由其内部成员registrar持有。
然后,ScheduledAnnotationBeanPostProcessor类实现了BeanPostProcessor接口,其中,postProcessAfterInitialization方法会扫描@Scheduled注解并创建相应的方法实例,该方法内部是通过调用processScheduled方法对注解@Scheduled的内容进行解析,processScheduled解析完@Scheduled后,会将其封装到相应的定时任务实例中,并将这些定时任务添加到ScheduledTaskRegistrar实例中。

一个完整的流程是:当 Spring 启动时,AbstractApplicationContext中的finishRefresh会触发所有监视者方法回调,其中,publishEvent会触发ScheduledAnnotationBeanPostProcessoronApplicationEvent方法(由于ScheduledAnnotationBeanPostProcessor实现了ApplicationListener,因此有该接口方法),查看下该方法源码:


public class ScheduledAnnotationBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent>,...{
    ...
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            if (event.getApplicationContext() == this.applicationContext) {
                // Running in an ApplicationContext -> register tasks this late...
                // giving other ContextRefreshedEvent listeners a chance to perform
                // their work at the same time (e.g. Spring Batch's job registration).
                finishRegistration();
            }
        }
    ...
}

可以看到,onApplicationEvent方法内部调用了finishRegistration方法,finishRegistration主要用于查找并设置TaskScheduler(注册调度器TaskScheduler),也就是 Spring 对异步任务的抽象封装类。其源码如下所示:

private void finishRegistration() {
    if (this.scheduler != null) {
        this.registrar.setScheduler(this.scheduler);
    }

    if (this.beanFactory instanceof ListableBeanFactory) {
        // 如果配置了定时任务 Bean: SchedulingConfigurer
        Map<String, SchedulingConfigurer> beans =
                ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
        List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
        AnnotationAwareOrderComparator.sort(configurers);
        for (SchedulingConfigurer configurer : configurers) {
            // 回调
            configurer.configureTasks(this.registrar);
        }
    }

    if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
        Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
        try {
            // Search for TaskScheduler bean...
            this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
        }
        catch (NoUniqueBeanDefinitionException ex) {
            logger.trace("Could not find unique TaskScheduler bean", ex);
            try {
                this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
            }
            catch (NoSuchBeanDefinitionException ex2) {
                ...
        }
        catch (NoSuchBeanDefinitionException ex) {
            logger.trace("Could not find default TaskScheduler bean", ex);
            // Search for ScheduledExecutorService bean next...
            try {
                this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
            }
            catch (NoUniqueBeanDefinitionException ex2) {
                logger.trace("Could not find unique ScheduledExecutorService bean", ex2);
                ...
            }
            catch (NoSuchBeanDefinitionException ex2) {
                logger.trace("Could not find default ScheduledExecutorService bean", ex2);
                // Giving up -> falling back to default scheduler within the registrar...
                logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
            }
        }
    }

    this.registrar.afterPropertiesSet();
}

finishRegistration方法的逻辑很清晰,它对定时任务调度器TaskScheduler的查找过程主要有 3 大步骤:

  1. 如果ScheduledAnnotationBeanPostProcessor本身设置了调度器,则将该调度器设置给ScheduledTaskRegistrar,具体代码如下所示:
if (this.scheduler != null) {
    this.registrar.setScheduler(this.scheduler);
}
  1. 如果用户配置了定时任务配置类SchedulingConfigurer,则回调配置类的configureTasks方法:
if (this.beanFactory instanceof ListableBeanFactory) {
    Map<String, SchedulingConfigurer> beans =
            ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
    List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
    AnnotationAwareOrderComparator.sort(configurers);
    for (SchedulingConfigurer configurer : configurers) {
        configurer.configureTasks(this.registrar);
    }
}
  1. 这是 Spring 默认的搜索行为,其具体搜索逻辑如下:
    1). 首先全局搜索唯一的TaskScheduler类型 Bean 实例:

    // Search for TaskScheduler bean...
    this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
    

    2). 如果存在多个TaskScheduler类型 Bean,则搜索名称为taskScheduler的 Bean 实例:

    private void finishRegistration() {
        ...
        this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
        ...
    }
    
    public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";
    private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) {
        if (byName) {
            T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType);
            if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory) {
                ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean(
                        DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName);
            }
            return scheduler;
        }
        ...
    }
    

    也就是说,如果存在多个TaskScheduler Bean 实例,则选择名称为taskScheduler的实例。

    3). 如果不存在TaskScheduler 类型 Bean 实例,就降级转而查找ScheduledExecutorService 类型 Bean 实例:

    this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
    

    4). 如果存在多个ScheduledExecutorService 类型的 Bean 实例,则查找名称为taskScheduler的 Bean 实例:

    this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
    

    5). 如果TaskSchedulerScheduledExecutorService Bean 实例都不存在,则结束查找。

    综上,finishRegistration查找逻辑为:优先查找全局唯一TaskScheduler Bean 实例,存在多个则选择名称为taskScheduler的实例,否则降级查找ScheduledExecutorService类型 Bean 实例,存在多个则选择名称为taskScheduler的那个 Bean 实例。

    对定时任务调度器TaskScheduler的查找都是通过方法resolveSchedulerBean进行的,事实上,默认情况下,Spring 在启动过程中,会自动帮我们注入一个TaskScheduler Bean 实例,对应的代码如下所示:

    private void finishRegistration() {
        ...
        this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
        ...
    }    
    
    private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) {
        ...
        else if (beanFactory instanceof AutowireCapableBeanFactory) {
            NamedBeanHolder<T> holder = ((AutowireCapableBeanFactory) beanFactory).resolveNamedBean(schedulerType);
            if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory) {
                ((ConfigurableBeanFactory) beanFactory).registerDependentBean(holder.getBeanName(), this.beanName);
            }
            return holder.getBeanInstance();
        }
        ...
    }
    

    所以默认情况下,Spring 提供了一个默认的TaskScheduler,其实我们前文也有提及,这个默认的TaskScheduler就是ThreadPoolTaskScheduler,所以默认情况下,会将ThreadPoolTaskScheduler注册给ScheduledTaskRegistrar,注册的方法如下所示:

    // ScheduledTaskRegistrar.java
    public void setTaskScheduler(TaskScheduler taskScheduler) {
        Assert.notNull(taskScheduler, "TaskScheduler must not be null");
        this.taskScheduler = taskScheduler;
    }
    

    我们对该方法进行单步调式,就可以看到ThreadPoolTaskScheduler默认的配置情况,如下图所示:

    taskScheduler

    可以看到,默认的定时任务调度器是一个名称为taskSchedulerScheduledThreadPoolExecutorScheduledThreadPoolExecutor实现了ScheduledExecutorService)线程池实例,且其线程数为1,线程前缀为scheduling-,所以默认情况下,定时任务都是串行运行的,且其线程名称都为scheduling-1,另一个需要注意的点是,默认的调度器其deamon = false,说明其是一个后台任务,即使应用主线程退出,定时任务仍然处于运行之中。

最后,finishRegistration方法末尾还调用了afterPropertiesSet方法,如下所示:

// ScheduledAnnotationBeanPostProcessor.java
private void finishRegistration() {
    ...
    this.registrar.afterPropertiesSet();
}

// ScheduledTaskRegistrar.java
@Override
public void afterPropertiesSet() {
    scheduleTasks();
}

所以afterPropertiesSet最终是调用的是scheduleTasks,见名知意,该方法用于调度已注册的定时任务,其源码如下所示:

// ScheduledTaskRegistrar.java
@SuppressWarnings("deprecation")
protected void scheduleTasks() {
    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));
        }
    }
}

private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<>(16);

private void addScheduledTask(@Nullable ScheduledTask task) {
    if (task != null) {
        this.scheduledTasks.add(task);
    }
}

可以看到,scheduledTasks方法对注册的不同的定时任务分别进行调度,调度的方法为scheduleXXXTask,比如,对于fixedRate的定时任务,其对应的调用方法为scheduleFixedRateTask,每次调度完成一个方法后,都会将调度结果通过方法addScheduledTask添加到一个集合中scheduledTasks
这里我们主要对调度方法进行分析,就分析一下scheduleFixedRateTask,其余的调度方法与之类似:

@Deprecated
@Nullable
public ScheduledTask scheduleFixedRateTask(IntervalTask task) {
    FixedRateTask taskToUse = (task instanceof FixedRateTask ? (FixedRateTask) task :
            new FixedRateTask(task.getRunnable(), task.getInterval(), task.getInitialDelay()));
    return scheduleFixedRateTask(taskToUse);
}

scheduleFixedRateTask(IntervalTask)方法内部最终是通过调用重载函数scheduleFixedRateTask(FixedRateTask)来完成调度,其源码如下:

@Nullable
public ScheduledTask scheduleFixedRateTask(FixedRateTask task) {
    ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
    ...
    scheduledTask.future =
            this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getInterval());
    ...
    return (newTask ? scheduledTask : null);
}

这里就可以看到了,最终是通过定时任务调度器taskSchedulerscheduleAtFixedRate来完成定时任务调度。

到此,源码分析基本已完成。

附录

  • cron 表达式:cron 表达式的语法如下所示:

    <秒> <分> <小时> <日> <月> <周> [年]

    其中,各字段允许的值和特殊符号如下表所示:

    字段 允许值 允许的特殊字符
    0~59 , - * /
    0~59 , - * /
    小时 0~23 , - * /
    日期 0~31 , - * ? / L W C
    月份 1~12 或者 JAN~DEC , - * /
    星期 1~7 或者 SUN~SAT , - * ? / L C #
    年(可选) 留空,1970~2099 , - * /

    上表中的特殊符号释义如下:

    • ,:表示枚举值。比如:假设小时为10,14,16,则表示上午 10 时,下午 2 时 以及 下午 4 时各触发一次。
    • -:表示间隔时间。比如:假设小时为8-12,则表示上午 8 时到中午 12 时每个小时时间段都进行触发。
    • *:表示匹配所有可能值。
    • /:表示增量。比如:假设分钟为3/20,则表示从第 3 分钟开始,以后每隔 20 分钟触发一次。
    • ?:仅被用于月和周两个子表达式,表示不指定值。
    • L:仅被用于月和周两个子表达式,它是单词“last”的缩写。如果在“L”前有具体的内容,它就具有其他的含义了,比如:假设星期字段为6L,则表示每个月的倒数第 6 天。
    • W:表示工作日(Mon-Fri),并且仅能用于日域中。
    • C:表示日期(Calendar)意思。比如:5C表示日历 5 日以后的第一天,1C在星期字段相当于星期日后的第一天。

    以下是一些常用的 cron 表达式:

    • 每隔5秒执行一次:*/5 * * * * ?
    • 每隔1分钟执行一次:0 */1 * * * ?
    • 每天上午10点,下午2点,4点:0 0 10,14,16 * * ?
    • 朝九晚五工作时间内每半小时:0 0/30 9-17 * * ?
    • 表示每个星期三中午12点:0 0 12 ? * WED
    • 每天中午12点触发:0 0 12 * * ?
    • 每天上午10:15触发:0 15 10 ? * *
    • 每天上午10:15触发:0 15 10 * * ?
    • 每天上午10:15触发:0 15 10 * * ?
    • 2005 2005年的每天上午10:15触发:0 15 10 * * ?
    • 在每天下午2点到下午2:59期间的每1分钟触发:0 * 14 * * ?
    • 在每天下午2点到下午2:55期间的每5分钟触发:0 0/5 14 * * ?
    • 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发:0 0/5 14,18 * * ?
    • 在每天下午2点到下午2:05期间的每1分钟触发:0 0-5 14 * * ?
    • 每年三月的星期三的下午2:10和2:44触发:0 10,44 14 ? 3 WED
    • 周一至周五的上午10:15触发:0 15 10 ? * MON-FRI
    • 每月15日上午10:15触发:0 15 10 15 * ?
    • 每月最后一日的上午10:15触发:0 15 10 L * ?
    • 每月的最后一个星期五上午10:15触发:0 15 10 ? * 6L
    • 2002年至2005年的每月的最后一个星期五上午10:15触发:0 15 10 ? * 6L 2002-2005
    • 每月的第三个星期五上午10:15触发:0 15 10 ? * 6#3

    比如在代码中配置如下:

    // 每隔 5s 执行一次
    @Scheduled(cron = "*/5 * * * * ?")
    public void cronTask(){
        logger.info("Thread[{}] - cronTask:{}", Thread.currentThread().getName(), new Date());
    }
    

    以上就通过 cron 表达式就设置了一个每隔 5 秒运行的定时任务。

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268