- 核心注解
@Scheduled:最简单、最常用的方式,基于 Spring 的任务调度。 - 核心注解
@EnableScheduling:启用定时任务功能。 - 配置方式:如何配置线程池,防止任务阻塞。
- 各种
cron表达式:如何定义任务的执行时间。 - 实践示例:一个完整的、可运行的代码示例。
- 进阶与替代方案:当
@Scheduled不够用时,有哪些更好的选择。
核心注解:@Scheduled
这是实现定时任务的核心,你只需要在需要定时执行的方法上加上这个注解,Spring 就会负责在指定的时间调用这个方法。

重要特性:
- 该方法不能有返回值(返回值会被忽略),并且可以接受参数。
- 默认情况下,它是单线程执行的,如果上一个任务的执行时间超过了任务间隔,那么下一个任务会等待上一个任务执行完毕后才开始,这可能会导致任务积压和延迟。
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component // 1. 必须是一个 Spring 管理的 Bean
public class MyScheduledTasks {
// 2. 在方法上添加 @Scheduled 注解
@Scheduled(fixedRate = 5000) // 每5秒执行一次
public void taskWithFixedRate() {
System.out.println("固定速率任务执行 - " + new Date());
// 模拟一个耗时3秒的任务
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
启用定时任务:@EnableScheduling
仅仅使用 @Scheduled 是不够的,你还需要告诉 Spring 应用:“我这里需要启用定时任务功能”,这个注解就是用来做这件事的。
我们会在一个配置类上添加这个注解,在 Spring Boot 项目中,最常见的就是在启动类上添加。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 启用定时任务功能
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
配置方式:解决单线程问题
如前所述,@Scheduled 默认是单线程的,这在生产环境中是一个巨大的隐患,假设你的一个任务需要10秒执行完,而你设置的 fixedRate 是1秒,那么任务队列会很快被堆积,导致系统响应缓慢。

解决方案:配置一个自定义的线程池。
我们需要创建一个 TaskScheduler 的 Bean 来替代默认的单线程执行器。
步骤:
- 创建一个配置类。
- 定义一个线程池
ThreadPoolTaskScheduler。 - 配置线程池参数(核心线程数、最大线程数、队列大小等)。
- 将这个
TaskScheduler声明为 Bean。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 使用一个固定大小的线程池来执行定时任务
// 这里我们创建一个拥有10个核心线程的线程池
taskRegistrar.setScheduler(taskExecutor());
}
@Bean
public TaskScheduler taskExecutor() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 设置线程池大小
scheduler.setThreadNamePrefix("scheduled-task-"); // 线程名前缀
scheduler.setWaitForTasksToCompleteOnShutdown(true); // 等待任务在关闭时完成
scheduler.setAwaitTerminationSeconds(60); // 等待时间
return scheduler;
}
}
配置好后,你的所有 @Scheduled 任务都会在这个线程池中并发执行,互不干扰。
cron 表达式详解
@Scheduled 注解提供了多种方式来定义任务执行时间,其中最强大、最灵活的就是 cron 表达式。
cron 表达式格式
一个标准的 cron 表达式由 6-7 个空格分隔的时间字段组成:
秒 分 时 日 月 周 [年]
| 字段 | 允许值 | 允许的特殊字符 |
|---|---|---|
| 秒 | 0-59 |
|
| 分 | 0-59 |
|
| 时 | 0-23 |
|
| 日 | 1-31 |
L W |
| 月 | 1-12 或 JAN-DEC |
|
| 周 | 1-7 或 SUN-SAT |
L |
| 年 | 1970-2099 |
特殊字符含义
- (星号): 代表所有可能的值。 在“分”字段表示“每一分钟”。
- (逗号): 列出枚举值。
MON,WED,FRI在“周”字段表示“周一、周三、周五”。 - (连字符): 定义一个范围。
10-12在“时”字段表示“10点到12点”。 - (斜杠): 定义一个步长。
0/15在“分”字段表示“从0分钟开始,每15分钟一次”。*/5等同于0/5。 - (问号): 只用于“日”和“周”字段,表示不指定值,当你需要指定其中一个,而另一个不关心时非常有用,想在每月的20号执行,但不管星期几,就可以写成
20 ? * * *。 L(Last): 表示“,在“日”字段中,L表示“一个月的最后一天”,在“周”字段中,L表示“一周的最后一天(星期六)”,6L或SAT都表示“最后一个星期六”。W(Weekday): 表示“最近的工作日”。15W表示“离15号最近的工作日(周一到周五)”,如果15号是周六,则会在14号(周五)触发;如果15号是周日,则会在16号(周一)触发。- 只用于“周”字段,表示“第几个星期几”。
6#3表示“每个月的第3个星期五”(6=星期五,3=第3个)。
常用 cron 表达式示例
| 表达式 | 含义 |
|---|---|
0 * * * * * |
每一秒执行 |
0 0/5 * * * * |
每5分钟执行一次 |
0 0 0/1 * * * |
每小时执行一次 |
0 0 12 * * ? |
每天12:00执行 |
0 15 10 ? * * |
每天10:15执行 |
0 15 10 * * ? |
每天10:15执行 (与上一个等价) |
0 15 10 * * ? 2025 |
在2025年的每天10:15执行 |
0 0 14 * * MON-FRI |
每周一到周五的14:00执行 |
0 0 0 L * * |
每月最后一天的24:00执行 |
0 0 0 1W * * |
每月第一个工作日的24:00执行 |
完整实践示例 (Spring Boot)
下面是一个完整的 Spring Boot 项目示例,包含了线程池配置和多种定时任务。
pom.xml
确保引入了 spring-boot-starter-web,它会自动引入所需的依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
启动类 SchedulingApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 启用定时任务
public class SchedulingApplication {
public static void main(String[] args) {
SpringApplication.run(SchedulingApplication.class, args);
}
}
线程池配置 SchedulingConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean
public TaskScheduler taskExecutor() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 线程池大小
scheduler.setThreadNamePrefix("scheduled-task-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(10);
return scheduler;
}
}
定时任务类 MyScheduledTasks.java
package com.example.demo.task;
import org.springframework.scheduled.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class MyScheduledTasks {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
// 1. 固定速率:每次执行开始后,5秒后再次执行,如果任务耗时超过5秒,则会并发执行。
@Scheduled(fixedRate = 5000)
public void taskWithFixedRate() {
System.out.println("固定速率任务执行于: " + dateFormat.format(new Date()));
}
// 2. 固定延迟:每次执行完成后,等待5秒,再执行下一次。
@Scheduled(fixedDelay = 5000)
public void taskWithFixedDelay() {
System.out.println("固定延迟任务执行于: " + dateFormat.format(new Date()));
}
// 3. 初始延迟+固定速率:首次延迟2秒后开始,然后以5秒的固定速率执行。
@Scheduled(initialDelay = 2000, fixedRate = 5000)
public void taskWithInitialDelay() {
System.out.println("初始延迟任务执行于: " + dateFormat.format(new Date()));
}
// 4. 使用 cron 表达式:每天上午10点15分执行
@Scheduled(cron = "0 15 10 * * ?")
public void taskWithCron() {
System.out.println("Cron 任务 - 每天10:15执行于: " + dateFormat.format(new Date()));
}
}
运行结果
当你启动这个应用时,你会在控制台看到类似下面的输出:
固定延迟任务执行于: 10:00:00
固定速率任务执行于: 10:00:00
初始延迟任务执行于: 10:00:02
固定速率任务执行于: 10:00:05
固定延迟任务执行于: 10:00:05
固定速率任务执行于: 10:00:10
固定延迟任务执行于: 10:00:10
...
你可以观察到,fixedRate 和 fixedDelay 的执行模式是不同的,并且它们都在我们配置的线程池中并发执行。
进阶与替代方案
@Scheduled 非常简单,但它也有一些局限性:
- 动态修改:修改
cron表达式需要重启应用。 - 分布式环境:在多个实例部署时,同一个任务会在所有实例上同时运行,导致任务重复执行。
- 失败重试:没有内置的失败重试和告警机制。
当遇到这些情况时,可以考虑以下更强大的方案:
Spring Integration + Quartz
Quartz 是一个功能非常强大和成熟的任务调度框架,Spring 对它进行了很好的集成。
- 优点:
- 功能强大,支持复杂的调度逻辑、集群、持久化。
- 可以动态修改任务。
- 缺点:
- 配置相对复杂。
- 引入了额外的依赖。
分布式任务调度框架 (生产环境首选)
在微服务架构下,最推荐使用专业的分布式任务调度框架。
- XXL-Job:
- 优点:轻量级、可视化调度中心、支持集群、故障转移、路由策略丰富、支持执行日志、邮件告警,是国内使用最广泛的框架之一。
- 核心思想:有一个调度中心,负责向各个部署了任务的应用(执行器)下发任务,执行器接收任务并执行,这样就能保证同一个任务在同一时间只有一个实例在执行。
- Elastic-Job:
- 优点:由当当网开源,基于 Zookeeper 实现分布式协调,支持分片(将一个大任务拆分成多个小任务并行执行)。
- 核心思想:通过 Zookeeper 的分布式锁和节点选举来协调任务的执行。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
@Scheduled |
简单的单机应用,任务量小,无分布式需求。 | 配置极其简单,开箱即用。 | 单线程,功能有限,无法动态修改,不适合分布式。 |
| Quartz | 功能要求较高的单机或小型应用。 | 功能强大,支持持久化和集群。 | 配置复杂,API 较重。 |
| XXL-Job / Elastic-Job | 微服务架构,分布式系统,需要高可用和高可靠性的场景。 | 真正的分布式调度,支持集群、分片、故障转移、监控告警。 | 需要额外部署调度中心,引入了外部依赖。 |
对于初学者和简单的个人项目,@Scheduled + 自定义线程池 是一个非常好的起点,对于任何生产级的、特别是分布式的系统,强烈推荐使用 XXL-Job 或 Elastic-Job 这样的专业框架。
