美文网首页功能整合
Spring Boot - 定时任务

Spring Boot - 定时任务

作者: Whyn | 来源:发表于2020-09-28 20:26 被阅读0次

    前言

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

    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 秒运行的定时任务。

    参考

    相关文章

      网友评论

        本文标题:Spring Boot - 定时任务

        本文链接:https://www.haomeiwen.com/subject/fugkuktx.html