1. 概述

在本篇文章中,我们将详细介绍如何使用 Spring 的 @Scheduled 注解 来配置和调度定时任务。

使用 @Scheduled 注解时,需要遵循以下简单规则:

  • 方法的返回值应为 void(如果非 void,返回值会被忽略)
  • 方法不能有参数

2. 启用定时任务支持

要在 Spring 中启用定时任务支持以及 @Scheduled 注解,可以通过 Java 配置类使用如下注解:

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

⚠️ 注意:默认情况下,Spring 的定时任务是单线程执行的,即使使用了 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);
    }
}

说明:加上 @Async 后,任务会并发执行,即使前一个任务还没结束,也会按频率继续触发。

5. fixedRate 与 fixedDelay 的区别

虽然都可以通过 @Scheduled 来调度任务,但 fixedRatefixedDelay 的执行逻辑完全不同:

  • fixedDelay:保证上一次任务执行结束后,再延迟 n 毫秒执行下一次任务。适用于必须串行执行的任务。
  • fixedRate:每 n 毫秒执行一次任务,不关心上一次是否执行完成。适用于任务之间互不依赖的场景。

⚠️ 踩坑提示:如果任务执行时间超过频率间隔,fixedRate 可能导致内存或线程池耗尽。

6. 初始延迟执行任务

有时候我们希望任务第一次执行前有一个延迟时间,可以使用 initialDelay

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

说明:第一次任务会在 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 执行任务。

默认使用服务器本地时区,但可以通过 zone 属性指定其他时区:

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

8. 外部化调度参数

硬编码调度参数虽然简单,但不便于维护。我们可以使用 Spring 表达式将配置外置:

@Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds}")
@Scheduled(fixedRateString = "${fixedRate.in.milliseconds}")
@Scheduled(cron = "${cron.expression}")

说明:通过 properties 文件配置参数,可以实现无需重新部署即可调整任务调度策略。

9. 使用 XML 配置定时任务

Spring 也支持使用 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. 动态设置延迟或频率

Spring 中 @Scheduled 注解的参数在启动时就已经确定,无法在运行时动态修改。

但可以通过实现 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);
              }
          }
        );
    }
}

说明:通过 Trigger 动态计算下次执行时间,可以实现运行时调整调度频率。

11. 并行执行任务

默认情况下,Spring 使用单线程执行所有定时任务。如果希望并行执行,可以自定义 TaskScheduler

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

11.1. Spring Boot 中的配置

如果使用 Spring Boot,可以直接通过配置文件设置线程池大小:

spring.task.scheduling.pool.size=5

12. 总结

本文全面介绍了如何在 Spring 中使用 @Scheduled 注解来配置定时任务,包括固定延迟、固定频率、Cron 表达式、动态调度以及并行执行等内容。

示例代码可参考:GitHub 项目地址


原始标题:The @Scheduled Annotation in Spring