核心概念
在 Spring 中,定时任务的核心是让某个方法在指定的时间点或时间间隔被自动执行,Spring 提供了多种方式来实现这一功能,主要可以分为以下几类:
@Scheduled注解:最简单、最常用的方式,由 Spring Framework 自身提供。- Spring Task Scheduler +
ThreadPoolTaskScheduler:@Scheduled的底层实现,提供了更灵活的配置,如动态修改 cron 表达式。 - Quartz 集成:功能最强大、最专业的调度框架,适合复杂的调度场景,如集群、持久化、失败恢复等。
@Scheduled 注解 (最简单)
这是 Spring Boot 中最推荐、最简单的方式,开箱即用。
添加依赖
在 Spring Boot 项目中,spring-boot-starter 已经包含了 spring-scheduling 依赖,所以通常不需要额外添加。
<!-- 如果你使用的是传统的 Spring 项目,需要手动添加 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<dependency>
启用定时任务
在 Spring Boot 的启动类上添加 @EnableScheduling 注解,告诉 Spring 容器去扫描并处理带有 @Scheduled 的方法。
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);
}
}
创建定时任务类和方法
创建一个 Spring Bean(用 @Component, @Service, @Controller 等注解标记),并在其方法上使用 @Scheduled 注解。
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class ScheduledTasks {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
// 1. 固定延迟执行:上一次执行完毕后,等待 fixedDelay 毫秒再执行下一次
@Scheduled(fixedDelay = 5000)
public void taskWithFixedDelay() {
System.out.println("Fixed Delay Task - The time is now " + dateFormat.format(new Date()));
}
// 2. 固定频率执行:每隔 fixedRate 毫秒执行一次,不关心上一次是否执行完毕
@Scheduled(fixedRate = 3000)
public void taskWithFixedRate() {
System.out.println("Fixed Rate Task - The time is now " + dateFormat.format(new Date()));
// 模拟一个耗时任务
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 3. 初始延迟:第一次执行前等待 initialDelay 毫秒,然后按 fixedRate 或 fixedDelay 执行
@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void taskWithInitialDelay() {
System.out.println("Initial Delay Task - The time is now " + dateFormat.format(new Date()));
}
// 4. Cron 表达式:最灵活的方式,可以指定具体的执行时间点
// 每天上午 10 点执行
@Scheduled(cron = "0 0 10 * * ?")
public void taskWithCronExpression() {
System.out.println("Cron Task - Executing at 10:00 AM daily. The time is now " + dateFormat.format(new Date()));
}
// Cron 表达式示例:
// "0 * * * * ?" - 每分钟的 0 秒执行
// "0/10 * * * * ?" - 每 10 秒执行一次
// "0 0/5 * * * ?" - 每 5 分钟的 0 秒执行
// "0 0 8-17 * * ?" - 每天 8点到17点,每小时的0分执行
// "0 0 0 L * ?" - 每月最后一天的 0 点执行
}
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 |
特殊字符说明:
- 匹配该域的任意值。 在“分”域表示“每分钟”。
- 只用于“日”和“星期”域,表示“不指定值”,当你想指定一个域,而另一个域不关心时使用,想在每月 15 号执行,但不关心是星期几,就可以写成
15 * * ?。 - 表示一个范围。
10-12在“时”域表示 10、11、12 点。 - 列出多个值。
MON,WED,FRI在“星期”域表示周一、周三、周五。 - 表示“起始时间/递增间隔”。
0/10在“秒”域表示从 0 秒开始,每 10 秒执行一次。 L:表示“Last”。- 在“日”域,表示“该月的最后一天”(28、29、30、31 都可能)。
- 在“星期”域,表示“7”或“SAT”。
- 可以和数字结合使用,如
6L表示“最后一个星期五”。
W:表示“工作日 (周一到周五)”,只能用于“日”域,它会找到离指定日期最近的工作日。15W表示如果 15 号是周六,则会在 14 号(周五)触发;如果是周日,则在 16 号(周一)触发。- 表示“该月的第几个星期X”,只能用于“星期”域。
6#3表示“该月的第三个星期五”。
优点
- 简单易用:代码侵入性低,只需添加注解。
- 开箱即用:Spring Boot 自动配置,无需复杂设置。
缺点
- 单线程执行:默认情况下,所有
@Scheduled任务都在一个单线程中执行,如果一个任务耗时较长,会阻塞其他任务的执行。 - 动态修改困难:在运行时修改 cron 表达式或启停任务比较麻烦。
- 不支持集群:在多实例部署时,同一个任务会在每个实例上都执行一次,导致任务重复执行。
ThreadPoolTaskScheduler (更灵活)
@Scheduled 的底层就是通过 ThreadPoolTaskScheduler 来实现的,通过自定义 TaskScheduler,我们可以解决单线程问题,并实现更高级的控制。
配置 ThreadPoolTaskScheduler
在配置类中定义一个 ThreadPoolTaskScheduler Bean。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.util.concurrent.Executor;
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean
public Executor taskExecutor() {
// 创建一个线程池调度器
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 设置线程池大小
scheduler.setPoolSize(5);
// 设置线程名前缀,方便排查问题
scheduler.setThreadNamePrefix("scheduled-task-");
// 等待所有任务完成后关闭调度器
scheduler.setWaitForTasksToCompleteOnShutdown(true);
// 初始化
scheduler.initialize();
return scheduler;
}
}
动态 Cron 表达式
如果需要在运行时动态修改任务的执行周期,可以结合 @Scheduled 和 @ScheduledAnnotationBeanPostProcessor 来实现,但这比较复杂,更推荐使用下面提到的 Quartz。
优点
- 解决单线程问题:通过线程池,可以并发执行多个任务,互不阻塞。
- 可控性强:可以精细地控制线程池的参数。
缺点
- 动态修改仍复杂:相比 Quartz,动态管理任务的能力较弱。
- 不支持集群和持久化:和
@Scheduled一样,不适合需要高可用和避免重复执行的场景。
Quartz 集成 (最强大)
Quartz 是一个功能齐全的开源作业调度库,是处理复杂调度需求的工业级标准。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
创建 Job (任务)
Quartz 的任务需要实现 org.quartz.Job 接口。
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.stereotype.Component;
@Component
public class MyQuartzJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 获取传入的参数
String jobName = context.getJobDetail().getJobDataMap().getString("jobName");
System.out.println("Quartz Job is running! Job Name: " + jobName + " at " + new Date());
// 在这里编写你的业务逻辑
}
}
配置和触发 Job
有两种方式来配置 Job:Java 配置 和 基于 properties 文件的简单配置。
方案 A:Java 配置 (推荐)
创建一个配置类来定义 Job 和 Trigger。
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuartzConfig {
// 1. 定义 JobDetail
@Bean
public JobDetail myJobDetail() {
// 关联我们的 Job 类
// 不持久化,每次调度都是新的实例
// 可存储数据
return JobBuilder.newJob(MyQuartzJob.class)
.withIdentity("myJob", "group1") // name, group
.storeDurably()
.usingJobData("jobName", "Hello Quartz Job") // 传递参数
.build();
}
// 2. 定义 Trigger
@Bean
public Trigger myJobTrigger() {
// 简单的触发器:每 5 秒执行一次
// SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever();
// 使用 Cron 表达式
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
return TriggerBuilder.newTrigger()
.forJob(myJobDetail()) // 关联 JobDetail
.withIdentity("myTrigger", "group1") // name, group
.withSchedule(cronScheduleBuilder)
.build();
}
}
方案 B:基于 application.properties 的简单配置
如果只是想快速启动一个简单的 Job,可以在 application.properties 中配置。
# 指定自定义 Job 的全限定名 spring.quartz.job-name=mySimpleJob spring.quartz.job-class=com.example.demo.MySimpleJob # 指定 Trigger 的 cron 表达式 spring.quartz.properties.org.quartz.scheduler.instanceName=MyScheduler spring.quartz.properties.org.quartz.threadPool.threadCount=5 spring.quartz.properties.org.quartz.triggers.myTrigger.cronExpression=0/10 * * * * ?
注意:这种方式功能有限,无法传递复杂的 JobDataMap。
Quartz 的优点
- 功能强大:支持 cron 表达式、简单触发器等。
- 集群支持:通过数据库(如 MySQL, PostgreSQL)作为 JobStore,可以实现集群环境下的任务调度,确保任务不重复执行且高可用。
- 持久化:任务信息可以被持久化到数据库,即使应用重启,任务状态也不会丢失。
- 动态管理:可以在运行时添加、删除、修改和暂停任务。
- 丰富的 API:提供了大量 API 用于控制调度器、任务和触发器。
Quartz 的缺点
- 配置复杂:相比
@Scheduled,配置要复杂得多,尤其是在集群环境下需要配置数据库。 - 依赖较多:引入了额外的依赖。
总结与选型建议
| 特性 | @Scheduled |
ThreadPoolTaskScheduler |
Quartz |
|---|---|---|---|
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 灵活性 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 性能 | 单线程,性能差 | 线程池,性能好 | 线程池,性能好 |
| 集群支持 | ❌ | ❌ | ✅ (通过数据库) |
| 持久化 | ❌ | ❌ | ✅ |
| 动态管理 | 困难 | 困难 | ✅ |
| 适用场景 | 简单的、非核心的、单机后台任务 | 需要多线程执行的单机任务 | 核心业务任务、需要高可用、集群环境、复杂调度逻辑 |
如何选择?
- 新手 / 简单场景:如果你的应用是单机部署,定时任务只是简单的日志清理、数据备份等非核心功能,首选
@Scheduled,它足够简单,能满足 80% 的需求。 - 需要多线程 / 单机性能优化:
@Scheduled的单线程问题导致任务阻塞,且你不需要集群,那么使用ThreadPoolTaskScheduler来配置一个线程池是最佳选择。 - 企业级 / 生产环境 / 核心业务:如果你的定时任务是核心业务逻辑(如定时结算、订单处理、报表生成等),或者你的应用需要部署在多台服务器上(集群),那么必须选择 Quartz,它能保证任务在集群中只执行一次,并能持久化任务状态,实现高可用。
希望这份详细的指南能帮助你更好地理解和使用 Spring 定时任务!
