Spring 中的 @Scheduled 注解

1、概览

本文将带你了解如何使用 Spring @Scheduled 注解来配置和调度定时任务。

使用 @Scheduled 对方法进行注解时,需要遵循如下简单的规则:

  • 方法的返回类型通常应为 void(如果不是,返回值将被忽略)
  • 方法不应有任何参数

2、启用定时调度

可以在配置类上使用 @EnableScheduling 注解来启用 Spring 中的定时任务和 @Scheduled 注解的支持:

@Configuration
@EnableScheduling
public class SpringConfig {
    ...
}

也可以在 XML 中启用,如下:

<task:annotation-driven>

3、以固定延迟调度任务

配置一个任务,使其在固定延迟后运行:

@Scheduled(fixedDelay = 1000)
public void scheduleFixedDelayTask() {
    System.out.println(
      "Fixed delay task - " + System.currentTimeMillis() / 1000);
}

如上,上一次执行结束与下一次执行开始之间的持续时间是固定的。任务会一直等待到前一个任务结束。

在必须确保上一次执行完成后再次运行的情况下,应使用此选项。

4、以固定频率调度任务

在固定的时间间隔内执行一项任务:

@Scheduled(fixedRate = 1000)
public void scheduleFixedRateTask() {
    System.out.println(
      "Fixed rate task - " + System.currentTimeMillis() / 1000);
}

如果任务的每次执行都是独立的,则应使用该选项。

注意,定时任务默认情况下不会并行运行。因此,即使使用了 fixedRate,在前一个任务完成之前也不会调用下一个任务。

如果想在定时任务中支持并行行为,就需要添加 @Async 注解:

@EnableAsync
public class ScheduledFixedRateExample {
    @Async
    @Scheduled(fixedRate = 1000)
    public void scheduleFixedRateTaskAsync() throws InterruptedException {
        System.out.println(
          "Fixed rate task async - " + System.currentTimeMillis() / 1000);
        Thread.sleep(2000);
    }

}

现在,即使前一项任务尚未完成,这项异步任务也会每秒被调用一次。

5、固定频率与固定延迟

可以使用 Spring 的 @Scheduled 注解运行定时任务,但根据属性 fixedDelayfixedRate,执行的性质会发生变化。

fixedDelay 属性可确保任务执行结束时间与下一次任务执行开始时间之间有 n 毫秒的延迟。

该属性在需要确保只有一个任务实例始终运行时非常有用。对于上次运行结果存在依赖的任务,它非常有帮助。

fixedRate 属性每 n 毫秒运行一次计划任务。它不会检查任务之前的任何执行情况。

如果任务的所有执行都是独立的,这就非常有用。但,需要确保不会超出内存和线程池的大小,如果传入的任务不能很快完成,有可能出现 “Out of Memory exception”。

6、使用初始延迟调度任务

接下来,调度一个有延迟(以毫秒为单位)的任务:

@Scheduled(fixedDelay = 1000, initialDelay = 1000)
public void scheduleFixedRateWithInitialDelayTask() {
 
    long now = System.currentTimeMillis() / 1000;
    System.out.println(
      "Fixed rate task with one second initial delay - " + now);
}

这个示例中同时使用了 fixedDelayinitialDelay。任务将在 initialDelay 值之后首次执行,并根据 fixedDelay 值继续执行。

7、使用 Cron 表达式调度任务

有时,仅靠延迟和频率是不够的,可能更需要 cron 表达式的灵活性来控制任务的时间表:

@Scheduled(cron = "0 15 10 15 * ?")
public void scheduleTaskUsingCronExpression() {
 
    long now = System.currentTimeMillis() / 1000;
    System.out.println(
      "schedule tasks using cron jobs - " + now);
}

如上,调度该任务在每月 15 日上午 10:15 执行。

默认情况下,Spring 使用服务器的本地时区作为 cron 表达式的时区。不过,可以使用 zone 属性来更改时区:

@Scheduled(cron = "0 15 10 15 * ?", zone = "Europe/Paris")

如上,Spring 将调度注解方法在巴黎时间每月 15 日上午 10:15 运行。

8、参数化调度时间

硬编码这些调度时间很简单,但我们通常需要能够控制调度时间,而无需重新编译和重新部署整个应用。

可以使用 Spring 表达式来外部化任务的配置,并将其存储在 properties 文件中。

fixedDelay 任务:

@Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds}")

fixedRate 任务:

@Scheduled(fixedRateString = "${fixedRate.in.milliseconds}")

基于 cron 表达式的任务:

@Scheduled(cron = "${cron.expression}")

9、使用 XML 配置调度任务

Spring 还提供了配置调度任务的 XML 方法。下面是设置这些任务的 XML 配置:

<!-- 配置调度器 -->
<task:scheduler id="myScheduler" pool-size="10" />

<!-- 配置参数 -->
<task:scheduled-tasks scheduler="myScheduler">
    <task:scheduled ref="beanA" method="methodA" 
      fixed-delay="5000" initial-delay="1000" />
    <task:scheduled ref="beanB" method="methodB" 
      fixed-rate="5000" />
    <task:scheduled ref="beanC" method="methodC" 
      cron="*/5 * * * * MON-FRI" />
</task:scheduled-tasks>

10、运行时动态设置延迟或频率

通常,@Scheduled 注解的所有属性只在 Spring Context 启动时解析和初始化一次。

因此,当在 Spring 中使用 @Scheduled 注解时,无法在运行时更改 fixedDelayfixedRate 值。

不过,有一个变通方法。Spring 的 SchedulingConfigurer 提供了一种更可定制的方式,让我们有机会动态设置延迟或频率。

创建一个 DynamicSchedulingConfig 类,并实现 SchedulingConfigurer 接口:

@Configuration
@EnableScheduling
public class DynamicSchedulingConfig implements SchedulingConfigurer {

    @Autowired
    private TickService tickService;

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

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
        taskRegistrar.addTriggerTask(
          new Runnable() {
              @Override
              public void run() {
                  tickService.tick();
              }
          },
          new Trigger() {
              @Override
              public Date nextExecutionTime(TriggerContext context) {
                  Optional<Date> lastCompletionTime =
                    Optional.ofNullable(context.lastCompletionTime());
                  Instant nextExecutionTime =
                    lastCompletionTime.orElseGet(Date::new).toInstant()
                      .plusMillis(tickService.getDelay());
                  return Date.from(nextExecutionTime);
              }
          }
        );
    }

}

通过 ScheduledTaskRegistrar#addTriggerTask 方法,可以添加一个 Runnable 任务和一个 Trigger 实现,以便在每次执行结束后重新计算 nextExecutionTime

还用 @EnableSchedulingDynamicSchedulingConfig 进行注解,以启动定时任务调度。

因此,调度方法 TickService#tick 在每次延迟后运行,延迟时间由 getDelay 方法在运行时动态决定。

11、并行运行任务

默认情况下,Spring 使用本地单线程调度器来运行任务。因此,即使有多个 @Scheduled 方法,每个方法都需要等待线程完成前一个任务的执行。

如果任务确实是独立的,那么并行运行它们会更方便。为此,可以提供一个更适合需要的 TaskScheduler

@Bean
public TaskScheduler  taskScheduler() {
    ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
    threadPoolTaskScheduler.setPoolSize(5);
    threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
    return threadPoolTaskScheduler;
}

在上例中,将 TaskSchedulerpool 大小配置为 5。但,实际配置应根据个人的具体需求进行微调。

11.1、使用 Spring Boot

如果使用 Spring Boot,可以使用更方便的方法来增加 Scheduler 池的大小。

只需在 application.properties 中设置 spring.task.scheduling.pool.size 属性即可:

spring.task.scheduling.pool.size=5

12、总结

本文介绍了在 Spring 应用中如何配置和使用 @Scheduled 注解来调度定时任务,还介绍了如何动态修改调度时间以及如何并行运行任务。


Ref:https://www.baeldung.com/spring-scheduled-tasks